Skip to content

meshio++ roadmap ​

Status at time of writing: v19.0.0, ABI 44. What ships is listed where it is documented rather than counted here, where counts went stale: the formats in supported formats and the conformance matrix, the operations in architecture, the language surfaces in the C API, Fortran, Julia, R, Octave / MATLAB and WebAssembly pages.

This document lists what is not built. Nothing here duplicates shipped functionality; where a feature partially exists, the shipped half is named and the gap is stated explicitly. Release history lives in CHANGELOG.md, not here.

Effort key: S = days, M = a couple of weeks, L = a month or more, XL = a project in its own right. An item names a probe — the failing test that proves the gap — wherever one is cheap.

How this file works ​

  • Sections are ordered, and the order is the recommendation. Each section states what belongs in it; an item that sounds exciting does not move up for that reason. An empty section is removed, not kept as a placeholder.
  • A closed item is removed, not struck through. Its history is the CHANGELOG.md entry and the feature's own doc/ page; a partly closed item is narrowed to what remains (the AGENTS.md change checklist rule).
  • Every level of the outline is numbered, so a gap can be cited in an issue, a commit or a release note: a group of work is section.item (a ###/#### heading, or a bold label line), the work under it section.item.subitem, and a bullet under that section.item.subitem.subsub. Lists that record a decision rather than open work — Considered, not queued, a section's Landing an item steps and the non-goals — stay unnumbered, because a number here means work to do. The numbering follows the sections, so a new section inserted at the top renumbers the rest, as the rule above says, and so does a section removed once its last item closes (Performance, October 2026: what was §4 onward moved up one; and again when reader and writer parity closed, the same month: operations, ecosystem reach and the full WebAssembly build became §3 to §5). The long-run section closed the same month as the last of its spikes landed (findings); being last, it renumbered nothing. Mesh generation (§6) closed the same month, also last: its native items landed, its measured-and-parked notes moved to mesh generation with their numbers and its references to mesh generation's further reading, its decisions to the non-goals, and its remaining dependencies to the sections they wait on (2.3, 3.3.1, 3.4), then 2.3, 3.2.1 and 3.3. The full WebAssembly build (§5) closed the same month, also last: its constrained primitive fitting landed (fit_cad's constrain_primitives), what it measured and left of skin to CAD moved to fit CAD's measured and parked notes, its decisions to the non-goals, and MMG's port to the browser to the section it waits on (3.3.8, then 3.2.8). Generation (§3.1) closed the same month: its primitive constructors landed on every surface, with full-Lagrange order=2 and the VTK Lagrange orders, the hand-rolled test helpers it left behind and why moved to parametric primitives, and its Deliberately not list to the non-goals; what was 3.2 to 3.4 became 3.1 to 3.3. Selection (§3.1) closed the same month, when select (3.1.3) landed on every surface beside the find_intersections and thickness it was listed with; what was 3.2 and 3.3 became 3.1 and 3.2. Volume mesh improvement (§3.2) closed the same month, when optimize_volume gained its worst-cell objective (3.2.1: guarded relocation, edge and multi-face removal, worst-cell smoothing), measured on Netgen's output in optimize_volume; being last, it renumbered nothing. The operations section (§3) closed with it, when MMG reached the full WebAssembly artifact (3.1.8: MMG in the full artifact, statically and without ELAS, its size measured there): ecosystem reach became §3, its items 4.1 to 4.7 now 3.1 to 3.7. In November 2026 an audit of the library against its own decisions, its performance and its ParaView plugin turned up two defects in the plugin, which opened Correctness debts as §1, so format reach, quality and ecosystem reach became §2 to §4 (what was 1.2.11 is now 2.2.11); and it appended Performance, the ParaView plugin and New capabilities as §5 to §7, the last taking the operators format reach's Considered, not queued had listed and the decisions recent work reopened. In November 2026 a review of the published feature list of ScanRuler, an AGPL-3.0 scan-inspection and reverse-engineering tool, appended Scan metrology and reverse engineering as §8 (ideas only; non-goals); being last, it renumbered nothing. New capabilities (§7) closed the same month, when its last item, the hexahedra of creased scans (7.9.2), was measured and parked in octree_mesh as a limit of the output after neither a smoothed skin nor a boundary let off it removed the cells, and 7.9.1 turned model_tolerances and virtual_faces on in v19.0.0: scan metrology became §7, its items 8.1 to 8.9 now 7.1 to 7.9. In November 2026 the first measure of duplication by place (tools/dup_report.py, while SonarCloud's first analysis was pending) appended Code duplication as §8, and SonarCloud's first analysis, the same day, SonarCloud findings as §9; being last, they renumbered nothing. The shared-utilities pass the same month closed 8.1, 8.5.2, 8.7.3, 8.8.1 to 8.8.6, 8.9.2 and 8.9.4: what was 8.2 to 8.11 became 8.1 to 8.10, their subitems renumbered within them. The binding pass the same month closed 8.1.2 to 8.1.4, 8.2.3, 8.3.1, 8.6.1, 8.8.1 and 8.8.2 and narrowed the rest: 8.3.2 became 8.3.1, 8.6.2 became 8.6 and 8.8.3 became 8.8.1; the sections kept their numbers. The third pass closed the rest but three, which became 8.1 (OpenCASCADE, what was 8.7.1), 8.2 (the subset and clean gather, what was left of 8.8.1) and 8.3 (installed headers, what was 8.9). The SonarCloud pass the same month worked through 9.1 to 9.4 as the first analysis had listed them and left the three things only the next scan settles, now 9.1 to 9.3. Scan metrology (§7) closed the same month, when its last items landed: the profile solids (7.7.4) through a CAD provider and the viewer's measuring mode (7.9.1), after the fits, elements and GD&T, alignment, deviation, thickness, sections to sketches, the reverse-engineering loop, scan editing and the MCP prompts; booleans with fitted solids (7.8.7) went to the non-goals, and its Considered, not queued list stays in the git history with the section. Code duplication and SonarCloud findings became §7 and §8, the latter's items 9.1 to 9.3 now 8.1 to 8.3. Since then a heading other pages link to carries an explicit anchor without its number ({#awaiting-a-licensed-run}, {#quality-of-implementation}), so a renumbering no longer breaks those links: link to the anchor, and cite the number in the text. The ParaView pass the same month closed Correctness debts (§1) and the ParaView plugin (§6): its payload layer closed 1.2, and 1.1 turned out to be backwards, the permutation meshio++ applied to linear wedges on the way to VTK being the defect (removed library-wide, ABI 44); the plugin's items 6.1 to 6.11 landed, 6.3.2 as a library change to the VTK writers. Format reach, quality, ecosystem reach and performance became §1 to §4 (what was 2.2.11 is now 1.2.11, 3.4 is 2.4, 4.1 is 3.1, 5.1 is 4.1), code duplication and SonarCloud findings §5 and §6 (8.1 to 8.3 now 6.1 to 6.3). A review of the whole library the same month, of its formats and operations against what its own pages say they leave out, of its tooling against what the workflows and the build actually run, and of the non-goals against what has shipped since each was decided, extended the sections in place (1.3 and 1.4, 2.6 to 2.17, 3.11 to 3.15, 4.12 to 4.15) and appended Data model as §7 and Operations as §8; being last, they renumbered nothing. It reopened five decisions, each narrowed where it is recorded and queued under the number named there, corrected the item numbers earlier renumberings had left behind in cross-references (which the test now listed as 3.16.2 would have caught), and marked every item it estimated from a doc/ page alone verify first. A second pass the same month put code anchors on every open item, so that taking one up needs no planning pass (landing checklists), and corrected against the code what the first had written from the pages: among others that CGNS polyhedra need no cgnslib, that the THREADS backend shipped in v19.0.0, that the R package cannot build on CRAN as it stands, and that the MMG and Netgen add-on wheels carry no LGPL text. It found that Ansys .rst records flagged both sparse forms (then 1.1.2) are decoded silently as one of them, which reopened Correctness debts as §1: format reach, quality, ecosystem reach, performance, code duplication, SonarCloud findings, data model and operations became §2 to §9, every item keeping its place within its section (what was 2.6 is 3.6, 8.1 is 9.1), except that the two remaining verification items of 1.1 became 2.1.1 and 2.1.2 and the chatbot's four parts became 4.4.1 to 4.4.5. The first slice of format reach closed the same month: the cgnslib reader reads every zone (what was 2.3.1.1) and OpenFOAM's compressed cases are read and written (what was 2.3.5.1), so 2.3.1.2 to 2.3.1.7 became 2.3.1.1 to 2.3.1.6 and 2.3.5.2 and 2.3.5.3 became 2.3.5.1 and 2.3.5.2, with 2.3.5.3 the compressed write's remaining surfaces.
  • Defect-shaped items go in a Correctness debts section at the top, regardless of size — behaviour that loses data, mis-orients cells, does not terminate on valid input or fails silently is not a feature request, even when the fix and the feature are the same work. The section is removed when its last item closes, which renumbers the rest, and the next defect found opens it again as §1.
  • An item estimated from a doc, a .d.ts or a changelog alone says so ("verify first") and names its probe; the code has repeatedly been more or less capable than its description.
  • Non-goals record decisions already taken, with their reasons, so they are not re-proposed as gaps.
  • Every open item is written to be picked up without a planning pass (November 2026). It states what exists today with the place in the code that proves the gap, what to reuse, ordered steps numbered so one can be closed alone, tests and fixtures, and a done when probe; a design-pass item lists the decisions to take and the option the code favours. Line numbers are those of master in November 2026 and drift: each is given beside a function, option or message name, which is what to search for. A claim an item could not check in the code says verify first.

Landing checklists ​

Two lists the items cite instead of repeating. They are what the most recent format (dxf) and operation (silhouette, fit_curves) commits touched; AGENTS.md holds the rules behind them. Sources and tests are globbed by CMakeLists.txt, so neither needs a CMake edit.

Checklist F: a format, or a new direction or construct of one.

  1. Core: src/cpp/include/meshioplusplus/formats/<fmt>.hpp and src/cpp/src/formats/<fmt>.cpp; header counts and tokens checked with detail/parse_guard.hpp; numbers through detail/fast_number.hpp and streams through detail/classic_stream.hpp; parameters through WriteOptions and ReadOptions.
  2. Registry: one row in registry_rows() of src/cpp/src/registry.cpp and the extension in its defaults table; for an optional dependency, the row inside the feature #ifdef and the library's name in registry_compiled_out.
  3. Python binding: core_def_read/core_def_write in bindings/python/_core.cpp (and a __has_<lib>__ flag for an optional dependency).
  4. Python twin: src/python/meshioplusplus/<fmt>/__init__.py (register_format, shims through _shims.plain_shims or core_declined) and _<fmt>.py; export in the package __init__.py (__all__ and the TYPE_CHECKING imports); python tools/gen_format_table.py.
  5. Tests: tests/cpp/test_<fmt>.cpp, tests/python/test_<fmt>.py (both engines, MESHIOPLUSPLUS_STRICT_CORE=1 for the core), a SPEC entry in tests/python/conformance_spec.py then python tools/gen_conformance_table.py, a seed in tests/fuzz/seed_main.cpp, and each reader crash fix's input under tests/fuzz/regressions/<fmt>/.
  6. Fixtures: tests/python/meshes/<fmt>/ with a README, the .gitattributes rule, and for third-party files the CITATION.cff paragraph and a CHANGELOG.md line; never a GPL fixture.
  7. Docs: doc/formats/<fmt>.md (Reading, Writing, what is not read), doc/formats.md, the sidebar in doc/.vitepress/config.mts, and the format lists of doc/c_api.md, doc/wasm.md and the other binding pages when the registry changes; the MCP server's format capabilities (mcp/_tools.py, tests/python/test_mcp.py, doc/mcp.md).
  8. Generated and release files: tools/amalgamate.sh; an ABI review row for a new or changed installed header; CHANGELOG.md; README.md; a notebook cell in example/python/ for a user-facing capability; narrow or remove the item here.

Checklist O: an operation.

  1. Core: src/cpp/include/meshioplusplus/operations/<op>.hpp (<Op>Options with m-prefixed members, <Op>Result {Mesh mMesh; …; std::vector<std::string> mWarnings;}, MESHIOPLUSPLUS_API <Op>Result <op>(const Mesh&, const <Op>Options&)) and src/cpp/src/operations/<op>.cpp, through the uniform mesh API, parallel_for for independent loops, shared helpers under src/cpp/src/detail/ recorded in common utilities.
  2. Python: m.def("<op>", …) in bindings/python/_core.cpp; src/python/meshioplusplus/_<op>.py (the core call, core_op_declined before a numpy twin, or _core_op for a core-only operation); three places in the package __init__.py (TYPE_CHECKING, _LAZY_ATTRS, __all__).
  3. C: mio_<op>_opts (size in its comment, a reserved tail), mio_<op>_opts_init, mio_<op>_report, mio_<op>, mio_<op>_warnings in bindings/c/include/meshioplusplus/meshioplusplus.h and bindings/c/c_api.cpp (the capi_guarded_* helpers and a static_assert on the struct size).
  4. The flat bindings: Fortran (bindings/fortran/meshioplusplus.f90: the public list, the bind(c) type, the wrapper); Julia (src/libmio.jl struct mirror, src/operations.jl, the export list in src/MeshioPlusPlus.jl); R (R/meshioplusplus.R, src/ops.c, src/init.c, NAMESPACE, man/mio_<op>.Rd); Octave (inst/+mio/<op>.m, src/ops_core.c; tests/python/test_octave_binding_parity.py holds it to R); WASM (bindings/wasm/js_bindings.cpp, src/wasm/src/index.mjs twice, src/wasm/index.d.ts; tests/python/test_wasm_parity.py holds it to the C header).
  5. CLIs: src/python/meshioplusplus/_cli/_<op>.py with a _Verb row in _cli/_main.py; cmd_<op> and a kCliVerbs row in src/cpp/cli/main.cpp; both checked by tests/python/test_cli_json.py.
  6. Pipeline, where the operation fits its contract: the step and its keys in tools/pipeline_steps.json, python tools/gen_pipeline_table.py, dispatch in src/cpp/src/operations/pipeline.cpp and _pipeline.py (tests/python/test_pipeline_spec.py).
  7. MCP: tool_<op> in mcp/_tools.py, its @_forward wrapper in mcp/_server.py, tests/python/test_mcp.py, the count in doc/mcp.md.
  8. Tests: tests/cpp/test_<op>.cpp, tests/cpp/test_c_api.cpp, a MIO_ABI_LAYOUT line in tests/cpp/test_abi_layout.cpp, tests/python/test_<op>.py, and the binding suites (tests/fortran, the Julia runtests.jl, R's testthat, Octave's tests/, tests/wasm/smoke.mjs).
  9. Measurement: a row("<op>", …) in src/cpp/benchmark/bench_ops.cpp, the determinism check (tools/bench_ops.sh … --hash), and benchmark/instructions_S.csv re-recorded with the reason.
  10. Docs and release files: doc/<op>.md, the sidebar, the operations table of doc/architecture.md, doc/{cli,c_api,fortran,julia,r,octave,wasm,mcp,pipeline}.md, the ABI row, the single header, a notebook in example/python/ with graphical output, README.md, CHANGELOG.md; narrow or remove the item here.

The map ​

The roadmap at a glance: open items grouped by section, shaded by effort, with dependency arrows and the items that need a design pass or a research spike first


1. Correctness debts ​

Admission: behaviour that loses data, mis-orients cells, does not terminate on valid input or fails silently (how this file works). Reopened in November 2026 by the pass that put code anchors on every item, which found the first item below while checking what was then 1.1.2.

1.1 Ansys .rst records flagged both bit-sparse and windowed-sparse are decoded as bit-sparse ​

Three files of Ansys' DPF example data hold records with both flags set; their layout is neither form's, both engines decode them as bit-sparse without a warning, and pymapdl-reader misreads them too. S to refuse, M to decode.

  • State today. src/cpp/src/formats/ansys_rst.cpp: the flags are kRstBsparse = 0x08 and kRstWsparse = 0x10 (lines 65–66), and RstFile::Record (lines 103–224) tests bit-sparse first (line 147) and windowed-sparse in the else if (line 164), so a record with both takes the first branch. The twin does the same: src/python/meshioplusplus/ansys_rst/_ansys_rst.py lines 87–90 (_bsparse line 100, _wsparse line 120). Ansys .rst says "dense or bit- and windowed-sparse" and does not mention the case. No test covers it (tests/python/test_ansys_rst.py has bsparse_doubles, tests/cpp/test_ansys_rst.cpp BsparseDoubles).
  • Steps.
    • 1.1.1 Refuse by name. In both engines, a record whose flags hold both bits raises ReadError through rst_fail naming the record pointer and the flags, unless 1.1.2 has landed. A hand-built record with both flags is the test in both suites; a fuzz seed goes under tests/fuzz/regressions/ansys_rst/. State it under Not read on the format page.
    • 1.1.2 Decode it. Find the layout: locate the three DPF example files (ansys-dpf-core's examples module downloads them; record their names and hashes), dump each both-flag record's header and length beside the dense values DPF returns for the same result set, and test the two candidate layouts (a windowed run inside each set bit, or a bit mask over windows). Needs a DPF Server (2.2.3's installation). When one layout reproduces DPF's values on all three, implement it in both engines and replace 1.1.1's refusal.
  • Done when. A file with such a record either raises by name or reads to DPF's values; pytest tests/python/test_ansys_rst.py -k both_sparse passes in both engines (MESHIOPLUSPLUS_STRICT_CORE=1 for the core).

2. Format reach ​

Admission: a format a simulation or physics-ML workflow actually exchanges; a shipped format's missing half is not a roadmap item, because every registered format reads and writes (decisions). "Exchanges" means a file that crosses a tool boundary in a real pipeline (mesher → solver, solver → post-processor, solver → training set), not a format that merely exists. Every item names the consumer on the other side of the file.

Sizes: S ≈ days, M ≈ two weeks, L ≈ a month or more, including tests, docs page and CLI wiring. They assume a new format reuses the shared components rather than growing its own: the card tokenizer (detail/keyword_card.hpp), the node-ordering registry, the Fortran record reader (detail/fortran_records.hpp) and the directory sniffing that sequence globs share.

Link legend: unmarked links were opened or returned by a search while this section was written (September 2026); links marked † are quoted from memory and must be checked before relying on them. Vendor documentation portals move often — when a link is dead, search the document title.


Every format this section queued is now read: the native FEM interchange, solver input and result-file readers of v16.5.0 to v16.12.0 (Tecplot, Femap, MFEM, Patran, Abaqus .fil, Radioss, Z88, libMesh, Marc, Ansys .rst, LS-DYNA and OP2), DOLFINx's ADIOS2 .bp (vtx) and Tecplot SZL through TecIO (szplt) of v16.13.0, and the vendor-runtime routes for the files only a vendor's software reads (Abaqus .odb, Marc .t16, Femap .modfem, Ansys results beyond the native reader). Since v16.17.0 the shared components are used wherever a format's layout fits them, the input decks are written too (Radioss's starter deck, checked with the OpenRadioss starter, and Marc's), Femap writes a series as one output set per step, and every format read but not written names its reason on the conformance page, which a test keeps in step with the registry; the missing half of each has since been written, so every registered format reads and writes. What they still lack is a check against a run of the vendor tool, listed under Awaiting a licensed run: take each as soon as a licence, or a file the tool wrote, is in hand.

Considered, not queued ​

  • Graphics, CAD, VFX or lab-specific rather than simulation interchange: X3D, VRML, Alembic, Silo, FBX, COLLADA, AMF, BYU, Rhino .3dm, Parasolid, OpenCASCADE .brep. DXF, once listed here, was reopened for sketches and outlines measured from a scan and ships as a drawing format (DXF); 3MF, E57, LAS/LAZ, OpenVDB and OBJ materials, listed here until November 2026, were reopened the same way, because the operations that produce or consume them have shipped since (2.4.5 to 2.4.8).
  • Building and city models, and GIS vector layers: IFC, CityGML and CityJSON, GeoJSON, Shapefile, GeoPackage. Their consumers want semantics (storeys, attributes, layers) a Mesh does not carry, and GDAL and IfcOpenShell serve them; WKT stays the one geometry exchange. Revisit with 9.10.2 if georeferenced meshes become a workflow.
  • Medical image volumes: NIfTI, NRRD and DICOM are voxel images, not meshes; an array from nibabel or SimpleITK reaches isosurface through grid today. The surface formats of the same pipelines are 2.4.4.
  • Closed CFD databases: STAR-CCM+ .ccm (libccmio is not redistributable) and .sim; FUN3D and FAST grids beyond what UGRID already reads.
  • Closed FEM databases with no route worth shipping: Altair .h3d (undisclosed format, SDK not redistributable — Altair's own advice for OpenRadioss is ANIM → VTK, see the OpenRadioss discussion); HyperMesh .hm; Abaqus .sim.
  • No native format to implement: deal.II (reads and writes UCD, VTK, Gmsh, Abaqus, Exodus — all reachable today); MOOSE (Exodus).
  • Nastran OP4 matrices and punch .pch: matrix and card dumps, not mesh/field exchange; pyNastran serves them.
  • Non-MSC Nastran HDF5 dialects: incompatible schemas; revisit with a file in hand.
  • Partitioning niche: gmsh $PartitionedEntities (Kratos partitions through MDPA).
  • CalculiX .dat print files: frd.read_dat stays a Python helper that returns a list of table dictionaries; it is not a Mesh, it has no writer, and .dat already belongs to Tecplot and Marc (CalculiX results).
  • Operators, not formats (listed only because they came up in the same review): mesh duals other than the tetrahedral median dual (now tet_to_poly). ICP registration, the cylindrical and spherical transforms and the shell completions, listed here with them, shipped as register_rigid, convert_coordinates and meshioplusplus completion.

Revisit any of them when a consumer asks with a file in hand — a real deck or result file, the tool version that wrote it, and the tool that needs the converted output.

2.1 Open verification items before coding ​

  • Where they are. In this file: 2.4.1 (the Fluent CFF readers), 2.4.6 (LASzip, laz-perf, libE57Format), 2.4.7 (NanoVDB), 9.1 (Manifold), 9.7 (METIS, Scotch). Outside it: twelve DOIs of mesh generation's further reading (doc/mesh_cad.md lines 322–363, legend at line 320) and one accessibility claim in doc/tui.md line 174. Not marked but of the same kind: femap.feFileWriteNeutral( in contrib/femap/export_neutral.py line 32 and the $!READDATASET line of contrib/tecplot/szplt_to_plt.mcr line 8, both written from the vendor references and never run.
  • Steps. Open each link and remove its † or replace the link; for a licence claim, quote the licence file's SPDX identifier and the version it was read at. The two vendor calls close with 2.2.4 and 2.2.5.
  • Done when. grep -rn "†" doc returns only the legends. S

2.1.2 MFEM NURBS meshes with non-conforming patches ​

  • State today. MFEM NURBS NC-patch mesh headers are refused: src/cpp/src/formats/mfem.cpp lines 2030–2031 (lex.Fail("'…' meshes are not supported"), after the accepted MFEM NURBS mesh v1.0/v1.1 branch at line 2022) and src/python/meshioplusplus/mfem/_mfem.py lines 831–832; tested at tests/python/test_mfem.py line 532 and tests/cpp/test_mfem.cpp line 396; stated in MFEM. The NURBS parser is mf_parse_nurbs/MfNurbs (mfem.cpp lines 1057–1403), its twin mfem/_nurbs.py.
  • Scope if a user asks. Read the patch_topology section with hanging knots, carry it in NurbsInfo (a new optional member: an ABI Tier A change), sample each patch as today (the non-conformity is between patches, so the sampled cells get hanging nodes: mark them as refine does, refine:hanging), and refuse iga_refine on such a model as it already refuses per-direction refinement of a conforming one.
  • Done when. MFEM's data/*-nc*.mesh NURBS samples read in both engines. No user has asked: do not start without one. M

2.2 Awaiting a licensed run ​

Checks that need the vendor tool itself, or a file it wrote that no public source has. Each reader works from the vendor's documentation and every public file found (September 2026); what remains is one probe per line. The procedure is the same for all of them, so it is stated once.

Recording a run.

  1. Run the probe the item names, on the vendor version it names or a later one.
  2. If the run produces a file worth keeping and its licence allows redistribution: put it under tests/python/meshes/<format>/ with a README.md in the form of tests/python/meshes/szplt/README.md (a <!--pytest-codeblocks:skipfile--> first line, who wrote the file with which tool and version, a table of files, the licence), add the directory's rule to .gitattributes (filter=lfs for large binaries as for nastran_op2/*.op2, or binary -filter to keep small ones out of LFS as for frd and szplt), and add a paragraph to the comment block at the top of CITATION.cff (source, copyright, licence, "redistributed unmodified") and a line to CHANGELOG.md. A file that cannot be redistributed is described on the page, with its hash, and a reference .npz of what the vendor tool reads from it is frozen by a tools/gen_<format>_reference.py in the pattern of tools/gen_vtx_reference.py.
  3. Turn the stand-in test into a real one beside it (the table names both), skipped when the fixture is absent, never deleted: the stand-in still guards the script's calls.
  4. Write the vendor version and the date into the page section the table names; where the table says add, create a ## Checked against section as Abaqus .odb has.
  5. Fix what the run finds in both engines, with the minimized input as a fuzz regression if it was a crash, then remove the item here and its clause in the non-goals' facts of the format if the run turned a fact into a feature.

The vendor-runtime routes of v16.13.0 are tested against stand-ins of the vendor APIs only (tests/python/test_contrib_routes.py, run by the optional-readers job of ci.yml). Each needs one recorded run; what to install for it:

  • 2.2.1 Abaqus .odb. Abaqus 2020 or later (its abaqus python, whose bundled numpy the script needs; 2023 and before run Python 2.7, 2024 and later Python 3). Run contrib/abaqus_odb/odb_to_vtu.py on a job with shells, C3D20R and an assembly-level set, and compare its nodal U and centroidal S against the same job's .fil read natively (route).
  • 2.2.2 Marc .t16. Marc/Mentat 2020 or later with PyPost (py_post, run with Mentat's Python). Run contrib/marc_t16/t16_to_vtu.py on a job that writes both .t16 and .t19, and compare against the .t19 read natively; confirm that post file position 0 holds the model (route).
  • 2.2.3 Ansys results via PyDPF. A DPF Server (Ansys 2021 R1 or later, or the standalone DPF Server 2024 R2 or later) and pip install ansys-dpf-core pyvista meshioplusplus[h5py]. Run contrib/ansys_dpf/rst_to_vtkhdf.py on a structural .rst and compare against the native .rst reader; then on a /FCOMP,RST,1 file, which the native reader refuses (route).
  • 2.2.4 Femap .modfem. Femap 2301 or later on Windows, and pip install pywin32. Run contrib/femap/export_neutral.py, check feFileWriteNeutral's argument list against that version's API help, and read the neutral file back (route).
  • 2.2.5 Tecplot .szplt. Tecplot 360 2023 or later, and pip install pytecplot. Save a data set as .szplt and as .plt from Tecplot itself and read both (the TecIO-backed reader against the native one); run contrib/tecplot/szplt_to_plt.py and the macro contrib/tecplot/szplt_to_plt.mcr; and point MESHIOPLUSPLUS_TECIO_LIBRARY at Tecplot's own shared libtecio for the ctypes reader (route). Also open in Tecplot 360 a file meshio++ wrote with single-precision, 16-bit and byte variables and one with partitioned zones; and, with a TecIO newer than 2018.3 (which refuses to write polygon and polyhedral zones in an SZL file), write such a zone, read it back, and check the written face map against Tecplot's reading, which would turn the refusal of those zones into a reader and a writer.

The readers:

  • 2.2.6 Tecplot. A .plt written by preplot from the ASCII fixtures (#!TDV112), read identically to its .dat, and the .plt meshio++ now writes opened in Tecplot or checked by preplot; binary files of versions 100–105 and 109–111, of which no sample was found (the VisIt suite covers 71, 75, 106–108).
  • 2.2.7 Femap. Element (404) and group (408) records written by Femap 10 and by 12 or later (the public 2401 files hold none); the slot layout of the 13-node pyramid (topology 19); a file meshio++ writes, a multi-set series included, imported into Femap.
  • 2.2.8 Patran. A neutral file exported by ANSA or HyperMesh.
  • 2.2.9 Abaqus .fil. A binary run with shells and C3D20R, checked against its .dat printout (the truss run TenBarArea is checked); a binary file of a single-precision build (4-byte words), of which no layout description or sample was found, which decides whether the reader and the writer take that width; and a file meshio++ writes, opened in Abaqus.
  • 2.2.10 Radioss. Animation files of the layouts before OpenRadioss (magic numbers 0x5426 to 0x542B): neither OpenRadioss's tools nor its readers document them. The 16-bit encoding of the nodal normals (the writer leaves them zero: anim_to_vtk ignores them and the encoding is undocumented), and a run of OpenRadioss's anim_to_vtk on a file meshio++ wrote, time-history lists included.
  • 2.2.11 Marc. A run of Marc itself, including on a deck or a .t19 post file (a remeshing series included) meshio++ writes (SIZING's fields, one type per ELEMENTS line, and the shell and plane-strain default types follow Volume C and Mentat's decks); post files before revision 9 (Marc K7 and earlier, Volume D 2000's PLDUMP layout); Volume A's edge and face numbering, to map edge and face sets (kept as (cell, number) field data) onto facets, which also decides whether side regions can be written to decks and post files; the binary .t16, read or written natively, which needs Volume D's PLDUMP description of it or a sample file (today it is read only through Marc's PyPost, by the route).
  • 2.2.12 Ansys .rst. A file written with /FCOMP,RST,1 or higher (zlib records): detail/zlib_inflate is ready, the record framing needs a sample. A harmonic (complex) result file, which no public sample or open reader covers (pymapdl-reader takes cyclic mode pairs only and only DPF reads complex sets), so complex results are neither read nor written; and a file meshio++ writes with element rotations and nodal coordinate systems, read by DPF (pymapdl-reader applies the three nodal rotations in the opposite order, so it agrees only on single-axis angles).
  • 2.2.13 LS-DYNA. A family from LS-DYNA with adaptive remeshing (NADAPT), the d3thdt time-history database, the cubic and bubble solids (21P, 15T, 20T, 40P, 64), airbag particles and rigid roads (read and written from the manual and lasso-python only: the airbag variable names of a real file, and rigid roads with the full rigid-body velocities, NDIM = 9, which the writer leaves out), a real d3part, thick shells, and the whole reader against LS-PrePost, with the families and binout files (links included) meshio++ writes opened there; whether LS-DYNA writes a ten-node solid's extra nodes once or twice.
  • 2.2.14 Nastran OP2. Random element tables, complex SORT2 nodal tables, CBUSH and CGAP forces, CBUSH real stresses, and NX's solid types 300–303 (read from pyNastran's layouts; no sample). Files meshio++ writes, opened in Femap, Patran or an NX/Simcenter post-processor: the writer produces the MSC dialect only, and the element tables it writes beyond pyNastran's reach (the SORT1 PSD, CRM and ATO random tables, the complex layouts pyNastran does not read) follow the reader's layouts.
  • 2.2.15 CalculiX frd. cgx -b (CalculiX GraphiX) reading a binary file meshio++ writes; ccx2paraview parses the ASCII layout only, so no open tool checks the binary variant beyond meshio++'s own reader and the binary files ccx wrote.
  • 2.2.16 FEBio .xplt. FEBio Studio or febio4 opening a file meshio++ wrote: the remeshed series (a mesh section per remeshed step), surfaces with per-surface-node variables, and the compressed states.
  • 2.2.17 Nastran HDF5. MSC's own post-processors (Patran, Simcenter) reading a file meshio++ wrote: its tables follow the column sets of the six MSC Nastran 2020 files, but the CBUSH, CGAP, CMASS and CONM1 element tables and the MAT5, MAT8, MAT9, PBARL, PBEAML and PCOMPG tables have no sample, so they are not written; a file with them would add them.
  • 2.2.18 DOLFINx VTX. ParaView's ADIOS2VTXReader (tools/gen_vtx_reference.py drives it) and DOLFINx reading a file meshio++ wrote with several cell types or parts (one block per type and part, with meshio.shared_points): both read one cell type per file, so this is meshio++'s own extension of the layout; and a VTXMeshPolicy.reuse file (ParaView 6.1 aborts on every step after the first of DOLFINx's own).

Where each run lands (sources are under src/cpp/src/formats/ with a twin under src/python/meshioplusplus/<format>/; tests under tests/python/ and tests/cpp/):

ItemScript or readerVersion recorded inStand-in or existing test
2.2.1contrib/abaqus_odb/odb_to_vtu.py (with contrib/common/mio_vtu_series.py)route Checked againsttest_contrib_routes.py::test_abaqus_odb_route, ::test_abaqus_element_table; compare with test_abaqus_fil.py
2.2.2contrib/marc_t16/t16_to_vtu.pyroute Checked against::test_marc_t16_route (the _PyPost stand-in)
2.2.3contrib/ansys_dpf/rst_to_vtkhdf.pyroute Checked against::test_ansys_dpf_route
2.2.4contrib/femap/export_neutral.pyroute: add the section::test_femap_route_calls
2.2.5contrib/tecplot/szplt_to_plt.py and .mcr; TecIO paths of tecplot.cpproute: add the section; szplt::test_tecplot_route_calls; test_szplt.py, test_szplt.cpp
2.2.6tecplot.cppTecplot Older versionstest_tecplot_plt.py, test_tecplot_plt.cpp; tools/gen_tecplot_plt_fixtures.py
2.2.7femap.cppFemap Verificationtest_femap.py, test_femap.cpp; tools/gen_femap_fixtures.py
2.2.8patran.cppPatran: add a Verification sectiontest_patran.py
2.2.9abaqus_fil.cppAbaqus .fil Validationtest_abaqus_fil.py, test_abaqus_fil_write.py
2.2.10radioss_anim.cpp, radioss_th.cppanimation Validation, time history Verificationtest_radioss_anim.py
2.2.11marc.cppMarc Validation and Not readtest_marc.py, test_marc.cpp; tools/gen_marc_fixtures.py
2.2.12ansys_rst.cpp (the zlib refusal is at lines 112–113)Ansys .rst Not read and Notestest_ansys_rst.py; tools/gen_ansys_rst_reference.py
2.2.13lsdyna_d3plot.cpp, lsdyna_binout.cpp (no d3thdt reader exists)d3plot What is not read and Verification; binout Verificationtest_lsdyna_d3plot.py; tools/gen_d3plot_reference.py
2.2.14nastran_op2.cppOP2 Verificationtest_nastran_op2.py, test_nastran_op2_write.py
2.2.15frd.cppCalculiX Binary outputtest_frd.py (3.2)
2.2.16xplt.cppFEBio Notestest_xplt.py
2.2.17nastran_h5.cppNastran HDF5 Verificationtest_nastran_h5.py
2.2.18vtx.cpp; tools/gen_vtx_reference.pyVTX Validationtest_vtx.py, test_vtx.cpp

2.3 Depth in formats already read ​

Constructs a registered format's reader refuses, skips or flattens. Each was located in the code (November 2026), not in a file: verify each first against a file the named tool wrote, and add the fixture with the fix. They are neither a missing direction nor an unwritten feature of something read (the parity decision covers both), except where an item says so; they are parts of the format nothing in meshio++ reads yet. Each lands in both engines by checklist F, with its Reading and Writing sections and its conformance declaration; sources are src/cpp/src/formats/<format>.cpp and src/python/meshioplusplus/<format>/.

2.3.1 CGNS ​

A CFD mesh arrives without its boundary conditions, its zone interfaces or its structured zones. Consumers: Fluent, STAR-CCM+, Pointwise, SU2 and every CFD post-processor. L in all; the sub-items are independent.

  • State today. Two core readers: the raw HDF5 path cgns.cpp (reads every zone, reads and writes NGON_n/NFACE_n since v9.21.0) and the cgnslib path cgns_mll.cpp (every zone too, in zone-name order, since 2.3.1's first step closed), tried first when MESHIOPLUSPLUS_WITH_CGNSLIB=ON (cgns.cpp lines 2141–2158), needed only for ADF containers and the CGNS 3.x inline-length NGON layout (refused by name at cgns.cpp lines 389–393). The h5py twin is cgns/_cgns.py and has no NGON (PARTIAL_ENGINE_EXEMPT["cgns"] in tests/python/conformance_spec.py). Conformance (SPEC["cgns"]): cell data missing, regions cell and point.
  • Reuse. h5:: helpers of src/cpp/include/meshioplusplus/detail/hdf5_util.hpp (open_group, group_links, read_dataset, read_attr_string); the file-local cgns_find_child_by_label, cgns_read_c1, cgns_write_node_attrs, cgns_write_c1_node, cgns_read_iterative_data (line 1424) and cgns_read_regions (line 885); detail::vtk_structured_cells (src/cpp/src/detail/vtk_xml_pieces.hpp line 20) for the lattice expansion; Region(name, RegionKind::Side, dim, tag, entries(n,2)).
  • Steps.
    • 2.3.1.1 Boundary conditions and families. BC_t, Family_t and FamilyName_t are ignored without a warning (no code names them). Read each ZoneBC_t/BC_t (PointList/PointRange, GridLocation Vertex, FaceCenter or CellCenter, ElementList for pre-3.3 files) into a region named after the BC, or after its FamilyName_t when it has one: a Point region for vertices, a Side region (cell, local facet) for faces found by matching the listed face elements to volume facets (detail/cell_faces.hpp), a Cell region when the BC names boundary elements kept as their own block. Keep the BC type (BCWall, …) in field_data["cgns:bc_type:<name>"]. Write them back, which also closes "side regions are not written" (the warning at cgns.cpp line 856 and _cgns.py lines 452–454). M
    • 2.3.1.2 Zone interfaces. ZoneGridConnectivity_t (GridConnectivity1to1_t, GridConnectivity_t): read the 1-to-1 pairs as point regions cgns:interface:<name> on both zones and offer weld through the existing merge path; abutting and overset interfaces are regions only. M
    • 2.3.1.3 Structured zones. Refused at cgns.cpp lines 1534–1538, cgns_mll.cpp lines 563–566 and _cgns.py lines 845–849. Expand GridCoordinates of (ni, nj, nk) to hexahedra or quads with vtk_structured_cells (the twin: vti._vti.lattice_hex_conn), map FlowSolution_t at Vertex and CellCenter, map structured BCs' PointRange to side regions, and record the dimensions in field_data["cgns:structured:<zone>"] so 8.6's annotation can write the zone back structured. Until 8.6 the writer writes it unstructured and the page says so. M
    • 2.3.1.4 More of FlowSolution_t. Rind: a solution padded with rind planes is refused by the row-count check (cgns.cpp lines 586–590, _cgns.py line 169): read Rind_t and strip the planes. FaceCenter/EdgeCenter solutions are skipped with a warning (cgns.cpp lines 1945–1952): they need 8.4. Solutions and regions of a multi-zone file are not read (cgns.cpp lines 1956–1990): concatenate per zone in block order, filling zones without the field as merge's Fill policy does. M
    • 2.3.1.5 Further bases. Bases after the first are ignored with a warning (cgns.cpp lines 1492–1493). Add a base read option (name or index, the region= precedent of OpenFOAM) through ReadOptions; reading all into one mesh stays out, since bases may differ in dimension. S
    • 2.3.1.6 ADF without cgnslib. Out of scope: ADF is a bespoke binary container and cgnslib is the route; state it on the page and correct the comments that still say polyhedra need cgnslib (CMakeLists.txt lines 374–386, registry.cpp lines 1254–1258, cgns/__init__.py lines 8–12). S
  • Tests and fixtures. tests/python/test_cgns.py, tests/cpp/test_cgns.cpp, tests/cpp/test_hdf5_formats.cpp; fixtures in tests/python/meshes/cgns/. The CGNS example set (cgns.github.io "Example files", public domain†) has multi-zone structured files with BCs: take two, with the README of step 2 of recording a run.
  • Done when. yf17.cgns and bump.cgns-class examples read with their BC regions in both engines, and meshioplusplus info lists them.

2.3.2 Exodus II ​

Global variables, set variables, polyhedral blocks and assemblies. Consumers: MOOSE (postprocessor values are global variables; its polyhedral output is NFACED), Sierra, Cubit, ParaView's IOSS reader. M

  • State today. Verified in the code: neither engine reads any of them. The reader is one loop over the netCDF variables (exodus.cpp from line 676, in read_exodus, line 626) with no branch for name_glo_var/vals_glo_var, name_nset_var/vals_nset_var*, name_sset_var/vals_sset_var* or the assembly groups, so they are ignored silently. NSIDED is refused ("Exodus: unknown element type", exodus.cpp lines 720–722, _exodus.py lines 274–276; the map exodus_to_meshio at lines 67–84 has no entry). NFACED blocks are stored under facconn{k}/fbconn{k}, which the connect prefix test never matches: probably dropped silently, which would be a correctness debt (verify with a file first, and move it to §1 if so). ExodusInfo holds mInfoRecords only (formats/exodus.hpp lines 67–76).
  • Reuse. The file-local netCDF helpers read_var, var_dims, read_names, read_att_text, read_ints, exo_time_values (exodus.cpp lines 151–266), which 2.4.3 moves to a shared header; ExodusTimeSeriesWriter (exodus.hpp line 122); the polyhedron kernel for NFACED.
  • Steps.
    • 2.3.2.1 Global variables. Read vals_glo_var(time, num_glo_var) at the selected step into field_data["exodus:global:<name>"] (the exodus:time precedent at lines 896–902); write field_data entries of that prefix back, in the series writer too. S
    • 2.3.2.2 NSIDED and NFACED. NSIDED: connect{k} with ebepecnt{k} node counts per element, to a ragged polygon block. NFACED: fbconn{k}/fbepecnt{k} (faces as node lists) and facconn{k}/ebepecnt{k} (elements as face lists), to a polyhedron block through the Face accessors. Write both; core only if the Python twin cannot stay byte-identical, as CGNS's polyhedra are (then a PARTIAL_ENGINE_EXEMPT entry). M
    • 2.3.2.3 Set variables. vals_nset_var{v}ns{s} is a value per node of a node set: scatter into a point array <name> that is NaN off the set, or keep as field_data["exodus:nset:<set>:<name>"] in set order; decide with 8.4, which gives side-set values (vals_sset_var{v}ss{s}) their home. Until 8.4, read both into the field_data form and write them back. S–M
    • 2.3.2.4 Assemblies. Exodus 8 assembly groups name a tree of blocks and sets: read into the hierarchy convention 8.5 decides; until then keep the tree in ExodusInfo so the writer returns it. S
  • Tests and fixtures. tests/python/test_exodus.py, test_exodus_writer_parity.py, tests/cpp/test_netcdf_formats.cpp. Fixtures: generate with netCDF4 in tests/python/exodus_fixture.py for 2.3.2.1 and 2.3.2.3; a MOOSE polyhedral output or a file from SEACAS' test data (BSD†) for 2.3.2.2.
  • Done when. ncdump -h of a file written back lists the same global and set variable names as the source.

2.3.3 EnSight Gold ​

Transient geometry, structured parts, several parts on write. Consumers: EnSight and ParaView reading what Fluent, STAR-CCM+, CONVERGE and OpenFOAM's foamToEnsight export. M

  • State today. ensight.cpp / ensight/_ensight.py. Transient (wildcard) geometry is refused (ensight_parse_case, lines 564–566; _ensight.py lines 232–233); change_coords_only is named nowhere; a structured block part fails with "expected 'coordinates' record" (line 673; _ensight.py line 447); only the eight kinds of kKinds are parsed (lines 493–496; _VARIABLE_KINDS), so complex and per measured node variables and the measured: line are ignored silently; only the first time set is honoured (lines 516–528). The writer (write_ensight, lines 1683–1749) writes no TIME section and one part 1, and ignores cell_data["ensight:part"], which its own reader produces for a file of two or more parts (lines 850–861): a round-trip loss, so 2.3.3.1 falls under the parity decision.
  • Reuse. ensight_resolve_wildcard (line 575) already zero-pads * runs for variable files; EnsightCaseInfo (line 420) holds the time values and file numbering; ensight_parse_geo (line 636); detail::vtk_structured_cells for block parts.
  • Steps.
    • 2.3.3.1 Write the parts that were read. Group cells by ensight:part and write one part per value, named from field_data or Part <n>; variables per part. S
    • 2.3.3.2 Transient geometry. Resolve the model: wildcard with ensight_resolve_wildcard at the selected step; honour change_coords_only (connectivity from the first file, coordinates from the step's). time_values and the sequence layer need no change. S–M
    • 2.3.3.3 block parts. Parse block [uniform|rectilinear] [iblanked] with its i j k header and expand; iblanked to a uint8 cell mask. S–M
    • 2.3.3.4 A transient writer, as 2.3.4 describes (the TIME section and numbered files).
    • 2.3.3.5 Measured and complex variables. Measured data is a point cloud with its own files: read as a second vertex block when a measured: line is present. Complex variables wait on 8.1's naming. S
  • Tests and fixtures. tests/python/test_ensight.py, tests/cpp/test_ensight.cpp; fixtures in tests/python/meshes/ensight/. Generate transient and block cases from ParaView's EnSightWriter or VTK's test data (BSD), and fix the stale comment at ensight.cpp lines 404–409 (tensors are read).
  • Done when. A two-part transient case from ParaView reads at every step and writes back with both parts.

2.3.4 A series writer for every format that reads one ​

MED, CGNS, Tecplot, Gmsh, EnSight and UNV read time steps, but a sequence written to them is refused, although each has an in-file timeline. S each

  • State today. sequence_write_supports_time (src/cpp/src/operations/sequence.cpp lines 282–306) lists 20 formats; these six are absent, though seq_format_may_have_steps (line 219) lists them on the read side. TEST(SequenceCapability, WriteSupportsTimeAgreesWithReality) (tests/cpp/test_sequence.cpp line 401) holds the list to what the sinks do.
  • The four places to touch per format. In sequence.cpp: the || chain and its message; kSeqSeriesFormats (lines 592–611); seq_make_series_sink (lines 696–770, SeqStepSink<TWriter> for a per-step Write(time, mesh) writer, SeqSink<TWriter, true> for a static-grid one). In Python: _SERIES_WRITERS and the write_sequence branch (src/python/meshioplusplus/_sequence.py lines 108–130 and from 850), with a shim on CoreSeriesWriter (_series_writer.py). The writer class copies FrdSeriesWriter (formats/frd.hpp lines 157–183: constructor, MESHIOPLUSPLUS_SERIES_WRITER_SPECIAL_MEMBERS, Write, NumSteps, Finalize, a pimpl), which is a pure ABI addition (a new class in an existing header: an ABI review row). C, Fortran, Julia, R, Octave and WASM reach it through the sequence entry points with no new symbol.
  • Per format.
    • 2.3.4.1 MED. The Python writer already writes several CHA steps, from array names "Name[idx] - pdt" or field_data["med:step_meta"] (med/_med.py lines 930–990, routed by med/__init__.py lines 111–150), but is not in _SERIES_WRITERS; the core writes one step with NDT = -1 (med.cpp lines 1820–1823, its comment deferring to Python). Give the core the same step groups, then wire both.
    • 2.3.4.2 CGNS. Write BaseIterativeData_t (TimeValues) and ZoneIterativeData_t (FlowSolutionPointers) with one FlowSolution_t per step; the read side is cgns_read_iterative_data (cgns.cpp line 1424).
    • 2.3.4.3 Tecplot. SzWriter::AddStep(mesh, Unsteady, Time) (tecplot.cpp lines 3084–3130) already writes one zone per step with a strand, for TecIO only; write_tecplot (line 2604) writes no SOLUTIONTIME or STRANDID. Write them in the ASCII and .plt zone headers, sharing the grid with VARSHARELIST/CONNECTIVITYSHAREZONE when it does not move.
    • 2.3.4.4 Gmsh. The time tag of $NodeData/$ElementData is hard-coded 0 (gmsh.cpp lines 2140 and 2176): write one data section per step with its time and step index.
    • 2.3.4.5 EnSight. The TIME section (number of steps, filename start number, time values) and one numbered variable file per step (ensight_resolve_wildcard gives the names); geometry once, or per step for a moving mesh.
    • 2.3.4.6 UNV. The writer already labels each 2414 dataset from unv:analysis, unv:step and meshio:time (unv.cpp lines 1995–2046): repeat that block per step.
  • Done when. For each: a three-step write_sequence reads back with its three times (a parametrized case in tests/python/test_sequence.py and test_sequence.cpp), and the capability test passes.

2.3.5 OpenFOAM ​

Sets, particle clouds, and compressed writes beyond Python and C++. S–M

  • State today. openfoam.cpp (4028 lines) / openfoam/_openfoam.py (single-region polyMesh only, PARTIAL_ENGINE_EXEMPT["openfoam"]). Both readers take writeCompression on cases (points.gz, …) and write_openfoam writes them through OpenFoamWriteOptions::mCompress and Python's compression="gzip" (compressed cases); nothing names sets or lagrangian. Tests build cases inline (tests/python/test_openfoam.py, tests/cpp/test_openfoam.cpp); there is no fixture directory.
  • Steps.
    • 2.3.5.1 polyMesh/sets. cellSet, faceSet and pointSet are labelLists like zones: parse with parse_zone_file_binary (line 624) and the ASCII zone path into regions set:<name> (face sets to side regions through the face owner, as face zones). Write regions of that prefix back as sets. S
    • 2.3.5.2 lagrangian clouds. <time>/lagrangian/<cloud>/positions (barycentric positions since v1806, or Cartesian coordinates) and the per-particle fields: a vertex block with point data, returned as a second mesh through a cloud= read option, since a particle is not a node of the mesh. Read only; a positions file needs the owning cell, which a writer would have to locate (9.3). M
    • 2.3.5.3 Compressed writes on the flat surfaces. The gzip write reaches only Python and C++, like the label and scalar widths: the registry's registry_write_ex (write_options.cpp, the openfoam branch) builds OpenFoamWriteOptions from WriteOptions, which has no compression field. Add one (an ABI change to WriteOptions, and mio_write_opts from its reserved tail), then --compression gzip in both CLIs and the WASM and flat-binding write options. S
  • Done when. A tutorial case's sets and a lagrangian cloud read in the core, and a blockMesh + simpleFoam case written by OpenFOAM with writeCompression on reads equal to its uncompressed twin (the synthetic gzipped cases of OpenFoamCompressed in tests/cpp/test_openfoam.cpp stand in until one is committed).

2.3.6 Gmsh ​

  • State today. $ElementNodeData, $InterpolationScheme and $GhostElements fall into the generic unknown-section skip with no warning: gmsh.cpp lines 1431–1432 (4.x body) and 1912–1913 (2.2), gmsh/_gmsh22.py lines 74–75, _gmsh41.py lines 91–100, _gmsh40.py line 83.
  • Steps.
    • 2.3.6.1 $ElementNodeData. A value per element per node (discontinuous fields: what Gmsh's own post-processing and GetDP write). Parse with the tag reader read_data (gmsh.cpp near line 943; common._read_data in Python) and store as cell data of shape (cells, nodes × components) under the convention 8.3 decides; until then under gmsh:element_node:<name> with the layout in field_data, the form abaqus:layout: uses. Write it back. S–M, after 8.3's design pass
    • 2.3.6.2 $GhostElements. To cell_data["partition:ghost"], the name partition's ghost layers use. S
    • 2.3.6.3 $InterpolationScheme. Kept only as an opaque GmshInfo member so a round trip does not lose it. S
  • Done when. Gmsh's tutorials/view*.msh-class file with element-node data round-trips in both engines.

2.3.7 The structured VTK family ​

  • State today. read_vtkhdf refuses every Type but UnstructuredGrid, PolyData and the two composites (vtkhdf.cpp lines 1636–1638; vtkhdf_is_leaf lines 163–165; vtkhdf/_vtkhdf.py line 830). .pvts, .pvtr and .pvti have no reader. vti, vtr and vts fail conformance with "no cell type round-trips" (conformance_spec.py, SPEC["vti"], ["vtr"], ["vts"]), because the canonical mesh is not a lattice; vtr needs a uniform one (vtr.cpp lines 50–52).
  • Reuse. detail::vtk_xml_read_pieces and vtk_structured_cells (src/cpp/src/detail/vtk_xml_pieces.hpp); detail::lattice_from_mesh and LatticeSpec (detail/grid_lattice.hpp lines 101–205); the parallel-index pattern of pvtu.cpp (pvtu_info line 70, pvtu_parse_index line 484) with pidx::merge_pieces (src/cpp/src/formats/pindex_common.hpp line 272), and in Python _parallel_xml.make_parallel_format and _pvtk_index.py.
  • Steps.
    • 2.3.7.1 VTKHDF ImageData. WholeExtent, Origin, Spacing, Direction attributes and PointData/CellData groups: expand as vti does. Then RectilinearGrid and StructuredGrid where the VTKHDF specification defines them at the pinned VTK version (verify first: they were proposals in 2025). S–M
    • 2.3.7.2 OverlappingAMR. Levels of boxes with a refinement ratio: read as one hexahedral block per level with vtkhdf:amr:level cell data and the coarse cells under finer boxes blanked into a uint8 mask; no hanging-node closure. HyperTreeGrid stays refused: it is a tree encoding with no cell list. M
    • 2.3.7.3 .pvts, .pvtr, .pvti. Add three rows to pvtu_info, reading pieces with the serial readers and placing each by its Extent in the WholeExtent so shared planes are one layer of points (the existing merge does not weld). Writers split by partition:part only if the parts are boxes; otherwise refuse by name. M
    • 2.3.7.4 The round trip is 8.6's: with its annotation, a mesh read from vts writes back as vts, and the three conformance declarations change from error to a note.
  • Done when. VTK's Testing/Data structured .vtkhdf and .pvti samples read to the same point and cell counts as vtkXMLReader reports.

2.3.8 Decomposed files, read whole without MPI ​

Reopened from the MPI non-goal, which stays as it is for the library: gathering the pieces of a finished run is file reading. M

  • State today. Four formats reassemble, each with its own code: OpenFOAM (reconstruct_decomposed, openfoam.cpp line 863, by the *ProcAddressing lists), pvtu (pidx::merge_pieces: no global ids, ghosts dropped, pieces labelled as piece_i regions rather than partition:part), Elmer (elm_read_parts, elmer.cpp line 541: nodes deduplicated by global id through a hash map, partition:part set) and MFEM (mf_read_parallel, mfem.cpp line 3372). Nothing names Nemesis, node_num_map or elem_num_map; the Exodus readers ignore the number maps. MergeOptions (operations/merge.hpp lines 66–70) welds by tolerance only.
  • Steps.
    • 2.3.8.1 One helper. detail/merge_by_global_id.{hpp,cpp}: pieces with a global node id per local node and optionally a global cell id, to one mesh with shared nodes merged, cells in global order when ids are given and cell_data["partition:part"] (kPvtuPartKey, formats/pvtu.hpp line 80). Move Elmer's and MFEM's loops onto it with digests unchanged (tools/bench_ops.sh --hash), record it in common utilities, and give pvtu the partition:part array beside its regions. S–M
    • 2.3.8.2 Nemesis. A base name with .e.<N>.<p> siblings (zero-padded p): sniff the family as the directory sniffing of sequence globs does, read each with read_exodus, take node_num_map and elem_num_map as the global ids, and merge node sets, side sets and variables. A read of any one member reads the family unless piece= selects one. Writing a family from partition:part follows, as OpenFOAM's decomposed=True. M
    • 2.3.8.3 CGNS and MED one file per rank. Only with a file in hand: neither has one convention for it.
  • Done when. A four-rank set from SEACAS decomp reads equal, after sorting by global id, to the serial file it was cut from.

2.3.9 Smaller losses, one fix each ​

Each is the parity decision's "unwritten feature" if the format can hold it; where it cannot, the page says so and the item closes. S each

  • 2.3.9.1 glTF morph targets. write_gltf writes one static mesh, and the reader skips morph targets (gltf_read.cpp lines 799–800) and animations (lines 832–836). Write a series of point displacements as targets (POSITION deltas) with an animation sampler on the weights, so a deformation series is one .glb a browser plays; read them back as steps. A series sink as in 2.3.4. M
  • 2.3.9.2 STL solid names and attribute bytes. solid <name> lines are treated as comments (stl.cpp lines 116–120, stl/_stl.py line 120); the two attribute bytes of a binary facet are skipped on read (lines 193–202) and written 0 (lines 365–366). Several ASCII solids become cell regions named after them and are written back as several solids; the attribute word becomes cell_data["stl:attribute"] (uint16) when any is non-zero.
  • 2.3.9.3 OBJ normal indices. vn rows are kept as point_data["obj:vn"] and written back; the per-corner vn index is parsed past and dropped (obj.cpp lines 184–196, _obj.py line 124), so normals that are not one per vertex are scrambled. Split points on distinct (v, vn) pairs as vt does (obj:parent_point).
  • 2.3.9.4 PLY. List-typed vertex properties are refused by the core (ply.cpp lines 211–212); the twin's header regex and ply_to_numpy_dtype["list"] (ply/_ply.py lines 111 and 182) look unable to read them either, against what the page says: test first. int64 properties are kept; the loss is connectivity cast to int32 on write (ply.cpp lines 489–494, _ply.py lines 473–483): refuse above 2³¹ rather than wrap, or write int64 lists behind an option.
  • 2.3.9.5 Medit normals. Normals, NormalAtVertices, Tangents and TangentAtVertices are discarded (kMeditCountedSkips, medit.cpp lines 570–579; medit/_medit.py lines 438–448 and 497–505). NormalAtVertices maps vertices to rows of Normals: read to point_data["medit:normal"] and write back. The engines also disagree on the skip itself, which is 3.4.12.
  • 2.3.9.6 Regions and data dropped on write. mphtxt drops point and side regions (mphtxt.cpp lines 702–705) and all data (line 874); MED side regions (med.cpp lines 901–904); frd regions and non-nodal arrays (frd.cpp lines 1093–1097 and 1287). For each, decide from the format's documentation whether it has a slot (COMSOL boundary selections; MED families on face elements; CalculiX has none) and write it or state the fact on the page.
  • 2.3.9.7 LS-DYNA *ELEMENT_SOLID_H20. Skipped as unsupported (lsdyna.cpp lines 419–422, lsdyna/_lsdyna.py lines 266–267); the two-line layout reads at most ten nodes. Read 20 nodes to hexahedron20 through the node-ordering registry.

2.4 New formats ​

Each names its consumer, as the admission asks. None was estimated from a file: verify first with one in hand, and with the licence of whatever reader the layout is taken from. Each lands by checklist F.

2.4.1 Fluent case and data ​

  • Why. The .msh mesh is read (ansys), but not a .cas/.dat pair nor the HDF5 .cas.h5/.dat.h5 (CFF) Fluent 2020 and later write by default: a Fluent user cannot bring a solution to meshio++ without exporting it first. Consumer: Fluent → ParaView, OpenFOAM or a training set.
  • Reuse. ansys.cpp: struct FluentReader (line 133: bracket depth, hex header groups, binary bodies), read_ansys (line 347: section index split into the binary-width prefix 20/30 and the core id; cores 10, 12 and 13 are nodes, cells and faces; 39 and 45 zone names), fluent_hex (284), fluent_face_rows (324); the twin's _Reader (ansys/_ansys.py line 53). HDF5 through detail/hdf5_util.hpp.
  • Steps.
    • 2.4.1.1 Legacy .cas. The same grammar as .msh with more sections: register .cas on ansys' reader, skip the solver sections by index, keep zone types (section 45) as region names and field_data["fluent:zone_type:<name>"]. S
    • 2.4.1.2 Legacy .dat. Sections 300, 2300 and 3300 ((300 (subsection zone size ntime nphase first last) (values…))): cell-zone values to cell data, face-zone values to the boundary blocks; the sub-section id to variable name table is in Fluent's xfile.h (documented in the Fluent User's Guide appendix; names only, no code). A .cas read looks for the sibling .dat, and data= names one. M
    • 2.4.1.3 CFF .cas.h5/.dat.h5. Undocumented; the open descriptions are VTK's vtkFLUENTCFFReader (BSD-3†) and PyFluent's file readers (MIT†): read the layout from them, not their code paths verbatim, and credit both in THIRD_PARTY_NOTICES.md only if code is taken. /meshes/1/{nodes,faces,cells} and /results/1/phase-1/{cells,faces}/<field>/1. A new format name (fluent_h5), gated on HDF5 as cgns is. M
    • 2.4.1.4 Writing. The parity decision asks for it: a case the reader returns is written back as legacy .cas/.dat (the .msh writer plus the data sections); CFF write only if Fluent is at hand to open it, otherwise a READ_ONLY conformance entry with that reason and an item in 2.2.
  • Fixtures. Fluent tutorial cases are not redistributable; VTK's Testing/Data has small .cas/.dat and .cas.h5 files (BSD): take those with their README.
  • Done when. VTK's room.cas/room.dat and room.cas.h5/room.dat.h5 read to the cell counts and field ranges vtkFLUENTReader and vtkFLUENTCFFReader report. M (legacy), M (CFF)

2.4.2 PLOT3D ​

  • Why. Multi-block structured grids (.xyz/.x/.g, solution .q, function .f): what OVERFLOW, CFL3D and NASA's turbulence-modelling resource exchange.
  • Reuse. detail/fortran_records.hpp: sniff_fortran_records, fortran_records, fortran_append_record, fortran_put_* (twin _fortran_records.py); vtk_structured_cells; the byteswap.hpp helpers.
  • Steps. (1) Sniff the variant, since the file does not state it: ASCII or Fortran-unformatted or raw binary, single or multi-block, 2-D or 3-D, single or double precision, with or without iblank, either byte order; sniff_fortran_records gives the record layout and the first record's size the rest. An ambiguous file takes read options (precision=, multiblock=, iblank=), named in the error. (2) One block per grid block, expanded to hexahedra or quads, iblank to a uint8 point array, block dimensions in field_data["plot3d:dims"] for 8.6. (3) .q: the four reference values to field_data, the five conserved variables to point data. .f: n named-by-index functions. (4) Writer: the unformatted multi-block double-precision form, from blocks that carry plot3d:dims; a mesh that is not a set of lattices is refused by name.
  • Fixtures. NASA TMR grids (US-government works).
  • Done when. A TMR multi-block .p3dfmt grid reads to its documented block dimensions and writes back byte-equal in the unformatted form. S–M

2.4.3 Coastal and ocean meshes ​

  • Why. Triangles and mixed polygons with nodal fields and a timeline, all of which the model holds. Consumer: a hydrodynamic run → ParaView, QGIS's mesh layer or a surrogate's training set.
  • Steps.
    • 2.4.3.1 A shared netCDF helper first. netCDF is used by exodus.cpp alone and its helpers are file-local (check, read_var, var_dims, read_names, read_att_text, read_ints; lines 58–266). Move them to src/cpp/src/detail/netcdf_util.{hpp,cpp} (core-private, in the manner of detail/hdf5_util.hpp), with Exodus on them and its digests unchanged; record it in common utilities. S
    • 2.4.3.2 UGRID-conventions NetCDF. The UGRID conventions: a variable with cf_role = "mesh_topology" names node_coordinates and face_node_connectivity (with start_index and a fill value for mixed polygons), optional edge and volume connectivities, and data variables with mesh and location (node, face, edge) attributes over a time dimension. Format name ugrid_nc (the AFLR3 ugrid keeps its name); .nc is sniffed by the attribute, since Exodus and others share the extension. Delft3D-FM, SCHISM, FVCOM, D-Flow and xugrid write it. Spherical lon/lat coordinates are kept as they are with the CRS name in field_data (9.10.2). Edge data needs 8.4. Python twin through netCDF4. M
    • 2.4.3.3 ADCIRC. fort.14 (ASCII: title, counts, nodes with depth, triangles, open and land boundary node strings to point regions) with fort.63/fort.64 ASCII results as steps; the NetCDF forms through 2.4.3.2's reader where they follow CF. Format name adcirc. S
    • 2.4.3.4 TELEMAC Selafin. .slf: Fortran big-endian records (fortran_records): title, variable names and units, IPARAM, counts, IKLE connectivity, IPOBO, coordinates, then per step a time and one record per variable; single or double precision by record size. Format name selafin. Units feed 9.10.1. S–M
  • Fixtures. The UGRID conventions' own examples; ADCIRC's public example problems; TELEMAC's examples (GPL data: do not copy, generate small files with the writer once it exists and check against a TELEMAC-written one locally).
  • Done when. A Delft3D-FM _map.nc, an ADCIRC quarter-annular case and a TELEMAC r2d_*.slf each read with their times in both engines. M for the three

2.4.4 Neuroimaging surfaces ​

  • Why. Head models for TMS and EEG simulation (SimNIBS, Brainstorm, MNE) mesh these surfaces into volumes; tetrahedralize is the next step of that pipeline. Lowest of this group: take it when a user asks.
  • Steps. (1) GIFTI (.gii): XML through pugixml (src/cpp/third_party/pugixml), DataArray elements with Intent (NIFTI_INTENT_POINTSET, _TRIANGLE, _SHAPE, _LABEL, …), Encoding ASCII, Base64Binary or GZipBase64Binary: decode with b64decode (detail/vtu_binary.hpp) then zlib_inflate with window 15 (detail/zlib_inflate.hpp), not the VTK block framing, which is a different header; ArrayIndexingOrder and Endian honoured; the coordinate-system matrix to field_data; label tables to regions. (2) FreeSurfer: binary triangle surfaces (magic 0xFFFFFE, big-endian), curv files (magic 0xFFFFFF) as point data, annot as regions; one format freesurfer with the sibling files as read options. (3) Writers for both.
  • Fixtures. The GIFTI reference files of the NITRC GIFTI project (public domain†); a FreeSurfer fsaverage hemisphere is under the FreeSurfer licence (do not copy: generate with nibabel in the test, importorskip).
  • Done when. nibabel reads what meshio++ wrote and the reverse. S–M

2.4.5 3MF ​

Reopened from Considered, not queued: print preparation is now a meshio++ workflow (repair, offset_surface's thicken and hollow, remesh, the scan-to-part loop), and 3MF is what slicers exchange, with units, several objects and colour where STL has none. Consumer: PrusaSlicer, Cura, Bambu Studio. M

  • Reuse. A ZIP reader exists but refuses more than one member: cmpin_zip_entry and cmpin_unzip in src/cpp/src/detail/compressed_input.cpp (lines 199–264; the refusal at line 237; no Zip64). For writing there is a CRC-32 (png_crc32, src/cpp/src/detail/png_write.hpp line 39) and deflate through zlib (png_write.cpp line 139). pugixml for the XML. In Python, zipfile.
  • Steps.
    • 2.4.5.1 A ZIP container in the core. detail/zip_archive.{hpp,cpp}: list members and read one by name (lifting the single-member limit of the existing parser, which then calls this), and write stored or deflated members with a central directory. No Zip64 (refuse above 4 GB by name), no encryption. Tests against zipfile-written archives; fuzz it through the 3MF reader's seed. Recorded in common utilities. S–M
    • 2.4.5.2 The core specification. Read _rels/.rels to find the model part (3D/3dmodel.model), then <resources>: each <object type="model"> with a <mesh> (<vertices>, <triangles v1 v2 v3 pid p1…>) is a triangle block and a cell region named after the object; <components> and the <build> items' transform are applied (transform), unit goes to meshio:length_unit. <basematerials> and per-triangle pid/p1 to a uint8 RGBA cell array 3mf:color. The writer emits one object per cell region, [Content_Types].xml and the relationships. M
    • 2.4.5.3 Out of scope. The materials, production, slice and beam-lattice extensions; a model using a requiredextensions the reader lacks is refused by name.
  • Fixtures. The 3MF Consortium's sample files (BSD-2†).
  • Done when. The consortium's validator (lib3mf's, run outside the tests) and PrusaSlicer open a file meshio++ wrote; the consortium's core samples read.

2.4.6 E57 and LAS/LAZ ​

Reopened: scan registration, surface reconstruction, deviation and the fitting operations shipped, and their input today is pcd, ply or xyz, while a terrestrial scanner writes E57 and an airborne one LAS. M

  • Reuse. pcd.cpp's shape for a point cloud: pcd_build_mesh (line 605) builds one vertex block (line 686), unpacks colour and keeps sensor metadata in field_data; conformance note "every node is kept as a vertex" (SPEC["pcd"]).
  • Steps.
    • 2.4.6.1 LAS, native. A fixed binary layout: the public header (versions 1.2 to 1.4, with scale and offset), variable-length records (the CRS as GeoKeys or WKT, kept verbatim for 9.10.2) and point records of formats 0 to 10: X Y Z scaled to doubles, intensity, classification, return_number, gps_time, colour. Read options to subsample (every=) and to pick fields. Writer: format 6 or 7 of LAS 1.4. Format name las. S
    • 2.4.6.2 LAZ. Compressed LAS through LASzip or laz-perf (Apache-2.0†), an optional dependency in the TecIO pattern: a CMake option MESHIOPLUSPLUS_WITH_LAZ, the feature macro, the registry row inside the #ifdef, registry_compiled_out returning the library's name so the refusal names it (registry.cpp lines 1236–1259), __has_laz__ in _core.cpp, the twin through laspy[lazrs], and a leg in the optional-readers job. S–M
    • 2.4.6.3 E57. XML header plus compressed binary sections; through libE57Format (BSL-1.0†) with the same gating (MESHIOPLUSPLUS_WITH_E57), the twin through pye57. One vertex block per data3D scan with its pose applied, or kept apart with scan=; intensity and colour to point data; images are not read. M
  • Fixtures. ASPRS sample LAS files and libE57Format's test data, each with its licence checked.
  • Done when. meshioplusplus info scan.las and scan.e57 list the points and fields, and register_rigid runs on two E57 scans.

2.4.7 OpenVDB, through NanoVDB ​

Reopened: compute_sdf, voxelize and octree produce the sparse level sets and fog volumes it stores, and physics-ML pipelines and Blender consume them. M, design pass first

  • State today. compute_sdf returns a hexahedral lattice mesh with sdf:distance point data and the lattice in field_data (sdf:origin, sdf:spacing, sdf:dims; operations/sdf.hpp lines 252–256), dense or as a 1-irregular octree (SdfStructure); LatticeSpec and lattice_from_mesh are in detail/grid_lattice.hpp.
  • Decisions to take first. (1) Whether NanoVDB's header (verify its licence, Apache-2.0 or MPL-2.0†, and that it is self-contained) reads and writes .nvdb only, which is what its viewers and Blender's importer need to be checked for, or whether .vdb proper is required, which means the OpenVDB library and its dependencies (then a provider add-on, not the core). (2) The shape on the meshio++ side: recommended, an export of a lattice array (a narrow band of sdf:distance, or voxelize's occupancy) and an import to a lattice mesh, registered as a format nvdb whose writer refuses a mesh that is not a lattice by name, as vti does.
  • Done when. Blender or nanovdb_viewer opens a narrow-band level set written from compute_sdf's output.

2.4.8 Materials and textures ​

Reopened: parameterize now produces texture coordinates, so a baked field or a scan's colour can travel as an image. S–M

  • State today. Texture coordinates already round-trip: uv (kUvName, operations/parameterize.hpp line 70) to OBJ vt with f v/vt corners (obj.cpp lines 225–303) and to glTF TEXCOORD_0 with v flipped (gltf.cpp lines 939–971). Missing: OBJ writes no mtllib/usemtl and reads none; glTF writes one fixed baseColorFactor material and no image (lines 1308–1340) and skips textures on read (gltf_read.cpp lines 835–836). A PNG encoder exists (png_encode_rgba, detail/png_write.hpp line 49); there is no PNG decoder.
  • Steps. (1) glTF: a texture= write option taking an RGBA array or a PNG path, embedded as a buffer view in .glb and referenced as baseColorTexture; on read, keep the image bytes in a side channel (GltfInfo) and its texture index, undecoded, so a round trip returns it. (2) OBJ: write mtllib and one usemtl with a sibling .mtl (map_Kd) and the PNG beside it; read usemtl groups as cell regions and the .mtl's Kd and map_Kd into field_data. (3) A bake_texture(mesh, array, size) helper that rasterizes a point or cell array through uv with the existing rasterizer and colormaps (detail/raster), so a field becomes an image: Python and the core, by checklist O. No material model beyond base colour.
  • Done when. gltf-validator (the gltf-validator CI job) accepts a textured .glb, and a parameterized mesh with a baked field opens textured in Blender.

3. Quality of implementation ​

Admission: work that makes every other item safer to land. None of it is a feature, so none of it competes for the same attention — it can run in parallel with everything.

The sanitizer leg, the reader fuzzing, the format conformance matrix, the property tests, the benchmark harness, the ParaView plugin test and the fallback narrowing all shipped in v16.14.0 (fuzzing and sanitizers, format conformance, benchmarks). What remains, each item with the files it touches; the build is one CMakeLists.txt (2,289 lines) and CI one ci.yml, so most items name lines in those two.

3.1 OSS-Fuzz submission ​

Calendar-bound, like the registries in §4. S

  • State today. The maintainer's Google-account contact is confirmed, the submission files pass OSS-Fuzz's presubmit checks, and the helper-container image and external-engine ASan build pass. The files are tools/fuzz/oss-fuzz/{Dockerfile,build.sh,project.yaml,package_seeds.py}: the image clones master with the Eigen and JSON submodules; build.sh configures with -DMESHIOPLUSPLUS_FUZZING_ENGINE="$LIB_FUZZING_ENGINE", HDF5, netCDF, gidpost and bzip2 off and the SEQ backend, copies meshioplusplus_fuzz_read once per format (from meshioplusplus_fuzz_replay -list-formats minus tools/fuzz/not_fuzzed.txt) and packs the seeds; project.yaml asks for address and undefined with libFuzzer. Fuzzing holds the helper commands, not a checklist; tests/python/test_fuzz_tools.py tests the tools; no workflow touches the directory.
  • Steps.
    • 3.1.1 In a checkout of google/oss-fuzz with the four files under projects/meshioplusplus/: python3 infra/helper.py build_image meshioplusplus, then for address and undefined: build_fuzzers --clean --sanitizer <s>, check_build --sanitizer <s>, run_fuzzer --sanitizer <s> meshioplusplus meshioplusplus_fuzz_read_vtu -- -max_total_time=30.
    • 3.1.2 build_fuzzers --clean --sanitizer coverage, unpack one seed corpus, coverage --no-corpus-download --no-serve --fuzz-target=meshioplusplus_fuzz_read_vtu, and read the report for readers with near-zero coverage (a missing seed or dictionary: add it to tools/fuzz/seed_corpus.py or tools/fuzz/dicts/).
    • 3.1.3 Add the two operation targets to build.sh (meshioplusplus_fuzz_mesh_cad_surface, meshioplusplus_fuzz_triangulate_polygon_2d; it builds the reader copies only) with their seeds from tests/fuzz/regressions/op_*.
    • 3.1.4 SECURITY.md first (3.14.1): OSS-Fuzz files reports against it. Then open the pull request in google/oss-fuzz, answer review, and turn the command block of the page into the record of what ran and when.
  • Done when. The project builds on oss-fuzz.com and its first reports arrive.

3.2 CalculiX validators in CI ​

  • State today. tests/python/test_frd.py::test_ccx2paraview_reads_what_the_writer_wrote (line 1404, five parametrized meshes) and TestBinaryWriter::test_ccx2paraview_cannot_read_the_binary_layout (line 1217) skip on importorskip("ccx2paraview") and importorskip("vtk"); no workflow installs either. CalculiX names ccx2paraview 3.2.0.
  • Steps. Add a frd-validator job to ci.yml in the pattern of gltf-validator (lines 1935–1970: needs: [lint], Python 3.12, pip install . plus the validator, one test file): pip install . ccx2paraview vtk pytest, pytest tests/python/test_frd.py, with MESHIOPLUSPLUS_REQUIRE_FRD_VALIDATOR=1 turning the skip into a failure as _require_validator does for glTF (tests/python/test_gltf.py lines 733–738). The frd fixtures are outside LFS, so the checkout needs none. Pin the ccx2paraview version and name it on the page.
  • Done when. The job is green and fails when ccx2paraview is uninstalled from it. S

3.3 The meshing corpora: sources and first numbers ​

The CAD corpus (benchmarks, tools/cad_corpus.py) and the Thingi10K sample tetrahedralize is measured on (tools/tet_corpus.py) are weekly jobs; three pieces need a run that can reach the sources, which this repository's development sandboxes could not. S

  • State today. cad_corpus.py has fetch (--manifest --dest --record), run, compare, baseline, table --check and summary; its manifest tools/cad_corpus/manifest.json takes git and https sources; fetch writes the hash of a model that has none to --record and refuses a mismatch; fetch_https (lines 176–189) extracts zip only. cad_corpus.yml runs Mondays and uploads corpus-hashes.json in the cad-corpus artifact. tet_corpus.py has fetch, run, summary; tools/tet_corpus/manifest.json has "models": {}, a seeded sample of 300 and the Hugging Face Thingi10K v1.5.0 tarball; tet_corpus.yml runs Tuesdays and uploads tet-corpus-hashes.json. Tests: tests/python/test_cad_corpus.py, test_tet_corpus.py.
  • Steps.
    • 3.3.1 NIST's MBE PMI test models (a US-government work): add an https source for the CTC and FTC STEP archives with no hashes, let the scheduled run record them, and commit the recorded sha256 values from the artifact.
    • 3.3.2 ABC (Koch et al. 2019; MIT dataset, models under Onshape's public-document terms, which permit fetch-at-run-time): its chunks are 7-Zip archives. Add archive: "7z" to fetch_https through py7zr (LGPL, a CI-only tool dependency, imported inside the branch with a named error) or the runner's 7z executable; a seeded sample of one chunk; a unit test with a tiny .7z built in the test.
    • 3.3.3 Thingi10K hashes and first numbers: take tet-corpus-hashes.json from the scheduled artifact into the manifest's models, and add a ## Measured on Thingi10K section to tetrahedralize, which has no place for them (its Measured section is at line 45), filled from tet_corpus.py summary, with a freshness test like test_the_published_table_is_fresh.
    • 3.3.4 What the CAD corpus still fails on is its measure for the mesher, not a task of this item: 399 of 16,503 faces at a size of the diagonal over 40 (degenerate patches 270, trims leaving their patch 69, other 60), all on KR600, the RC buggy and the Ventilator.
  • Done when. Both manifests carry hashes for every model and the two pages their tables.

3.4 Code cleaning, ordered by SonarCloud's report ​

The master scan (static analysis) ranks files by duplicated-lines density; an item closes when its copies call the shared kernels, whose history is in shared geometry and FEM kernels. The copies that need no kernel are §6. M–L

  • The shared targets (all core-private under src/cpp/src/detail/ unless noted): math_utils.hpp (MathUtils::Sub, Add, Scale, Dot, Cross, Norm, Distance, UnitByDivision with a zero guard, AngleDeg, SolveSymmetric3); geometry_utils.hpp (TriangleNormal, TriangleTwiceArea, TriangleArea, TriangleAnglesDeg, SignedTwiceArea2D); bounding_box_utils.hpp (BoundingBox3, Expand, FromPoints seeded with the first point, FromCoords, Extent, Diagonal); the installed include/meshioplusplus/detail/geometry.hpp (Vec3, vec3_*, triple_product, det3); fem_kernels.hpp (tet_volume6, tet_volume, det_unit3, corner_jacobian_ratio, kHexCornerEdges, kWedgeCornerEdges); the installed detail/cell_faces.hpp, cell_edges.hpp (surface types only) and cell_subdivision.hpp (cell_refine_edges, which holds the volume edges); bspline_ndu.hpp; always_inline.hpp (MIO_ALWAYS_INLINE).
  • Probe for every sub-item. tools/dup_report.py --lang cpp --json out.json shows the file's duplicated lines dropping; tools/bench_ops.sh with --hash and a BASELINE reports identical digests; python3 tools/bench_instructions.py --check benchmark/instructions_S.csv passes. A copy whose arithmetic differs from the kernel changes the last bits of its results, so it moves only when that change is shown not to happen or is intended and recorded. Each new or extended utility gets its tests/cpp/test_*_utils.cpp case and its row in common utilities.
  • 3.4.1 The remaining vector arithmetic. M
    • Hand-written cross products in about 30 sources, for MathUtils::Cross: operations/mesh_cad.cpp (lines 510, 2192, 2207, 3046, 3051, 4575, 5660, 6435), detail/step_brep.cpp (418, 1047, 1222, 1300, 1314, 1372), operations/fit_cad.cpp (99, 379, 555), tri_tri.cpp, tet_poly.cpp, render.cpp, nastran_model.cpp. Find the rest with a regex on a[1]*b[2] - a[2]*b[1].
    • Unit vectors without a zero guard, which the shared guard would change on a zero vector: coord_unit (operations/coordinates.cpp line 45), pf_normalize (detail/patch_flatten.cpp line 37). Decide per site whether a zero input can occur; if not, share and assert.
    • Lambdas: cross and unit in rad_skew_axes (formats/radioss.cpp lines 259–260), dot in bspline_lsq.cpp (347) and edge_sampler.cpp (145).
    • Bounding boxes seeded with ±infinity or ±1e300: about 50 loops in 32 files (brep_nurbs.cpp 318, external_mesher.cpp 252, fit_cad.cpp 299 and 402, reorder.cpp 226, stats.cpp 107, measure.cpp 184, tetrahedralize.cpp 478, six in feature_fit.cpp). They differ from BoundingBoxUtils::FromPoints on NaN and on an empty set: add a FromPointsOrEmpty (infinity-seeded) beside it rather than change either behaviour.
    • 31 local aliases of std::array<double, 3> (QrV3, PatchVec, SfVec, LsV3, MbVec, CoordVec, UtgVec, TetV3, GeoVec, MphVec, …), for Vec3: mechanical, one commit, no digest can move.
  • 3.4.2 Volumes and scaled Jacobians. M
    • Hand-expanded determinants whose expansion order differs from triple_product: utg_measure and utg_scaled_jacobian (operations/untangle.cpp lines 65 and 99), ls_corner (detail/layer_sweep.cpp 43), tp_signed_volume (operations/tet_poly.cpp 181), rvol_tet_ok (operations/remesh_volume.cpp 397). Each either moves with a recorded digest change or stays, listed in 3.4.11.
    • Quad scaled Jacobians: quad_scaled_jacobian (quad_recombine.cpp 67), quality_quad (quality.cpp 187, the ratio at 224): one GeometryUtils::QuadScaledJacobian.
    • Closed-surface and polyhedral volumes outside poly_measure: hds_hex_volume (hex_dual_schemes.cpp 1380), closed_surface_volume (detail/mesh_boolean.cpp 550), repair_signed_volume (repair.cpp 425), quality_facefan_volume (quality.cpp 126), and the loops at octree_mesh.cpp 128, hull.cpp 543, conservative_interpolate.cpp 391 and providers/occt/fit_cad_occt.cpp 2696: one GeometryUtils::ClosedSurfaceVolume(points, triangles) (9.4 needs the same integral's higher moments).
    • kUtgHexCorner (untangle.cpp 51) repeats kHexCornerEdges with the corner prepended.
  • 3.4.3 Shape functions and quadrature. Option A of the design. L
    • Today: ts_lagrange1d, ts_corner_weights, ts_shape_functions (operations/tessellate.cpp lines 174–205, five quadratic types, the serendipity flag at 72); ct_shape and curved_triangle_jacobian (detail/curved_triangle.cpp 40 and 231); cq_basis, cq_basis_d (detail/curved_quad.cpp 33 and 40); mf_serendipity_shape (formats/mfem.cpp 2618); and the general layer that already exists, formats/lagrange_common.hpp (namespace lagrange: basis, interpolation_matrix at line 721, fixed_lagrange to order 10, the orthogonal Legendre and Dubiner bases at 629–649), with its twin _lagrange.py.
    • Step 1: move lagrange_common.hpp from formats/ to detail/reference_element.hpp (it is not format code; 9.5 needs it from an operation) and add first derivatives. Step 2: the four special-case bases call it, each behind the digest check. Step 3: quadrature in one detail/quadrature.hpp: Gauss–Legendre gauss_legendre(N, …) (detail/bspline_lsq.cpp 192) and the 5-point table (detail/edge_sampler.cpp 43–46) move there; add the simplex and tensor rules for triangle, quad, tet, hex, wedge and pyramid to the order the readers' integration-point data uses (8.3 names its rules from this table).
  • 3.4.4 Simplex FEM pieces. M
    • P1 stiffness: mph_simplex_stiffness (operations/morph.cpp 183) and sobo_assemble (operations/sobolev_deform.cpp 184), to fem_kernels.hpp.
    • Cotangent weights: detail/surface_fem.cpp lines 83–119 and curv_corner/curv_dual_area (operations/curvature.cpp 60 and 89).
    • The per-triangle gradient inline in geodesic_distance (operations/geodesic.cpp 223–237).
    • Barycentric coordinates: interp_tet_contains and interp_tri_contains (interpolate.cpp 169 and 209), if_barycentric (interfaces.cpp 385), Locate (streamlines.cpp 308), McadDisc::Bary (mesh_cad_reparam.cpp 156), rst_prepare_tri (raster.cpp 106): one GeometryUtils::TetBarycentric/TriangleBarycentric with the containment tolerance a parameter, since the sites differ in it (9.3's locator is the sixth user).
    • Tet dihedral angles: tet_dihedral_angles (detail/bcc_octree_tets.cpp 105) is already what octree_mesh_optimize calls; tet_dihedrals (tetrahedralize.cpp 61), optvol_dihedrals (optimize_volume.cpp 942) and quality.cpp 348–350 remain. optvol_min_dihedral (438) stays: it is the measured hot loop of 5.5.2.
    • 3×3 solves: gradient already calls SolveSymmetric3; only quality_solve3 (quality.cpp 265, Cramer's rule, non-symmetric) remains, for a MathUtils::Solve3. metric.cpp and transform.cpp have none.
  • 3.4.5 Topology tables. Read from cell_faces and cell_refine_edges instead of typing them again, where the order agrees. S–M
    • Same order as the shared table, so mechanical: kOctmtFacet (octree_mesh_tetra.cpp 55), kOctmoFace (octree_mesh_optimize.cpp 54), kFaces (conservative_interpolate.cpp 281), kE (cut.cpp 310).
    • A different order, so the loop's iteration order and with it tie-breaks and digests would change: kFaces (tetrahedralize.cpp 62), fverts (quality.cpp 349), the triangulated faces of boundary_layer.cpp 135–142, the tet edges {0,1},{0,2},{0,3},… of optimize_volume.cpp (310, 442, 584, 945), kTetEdgeCorners (decimate_volume.cpp 311) and kTetEdge (detail/marching.cpp 57), and hex_dual_schemes.cpp's lexicographic kHdsFaces/kHdsEdges (121–123). For these add the order as a named second table in cell_subdivision.hpp (tet_edges_lexicographic) and share that; do not reorder.
  • 3.4.6 B-spline bases. Mostly closed: nurbs_find_span and nurbs_basis (detail/nurbs_eval.hpp 45 and 73) are what bspline_curve.cpp, bspline_lsq.cpp and iga_kernels.hpp call. bn_span and bn_basis (detail/brep_nurbs.cpp 42 and 60) remain and behave differently (no repeated-knot skip; a zero-denominator guard nurbs_basis lacks): decide which behaviour is right on a knot of full multiplicity, test it, then share. nurbs_basis itself does not use bspline_ndu. S
  • 3.4.7 A geometry abstraction, decided by use. The design note recommends kernels over a Kratos-style class hierarchy, and a non-virtual GeometryView over them when call sites want it; reopen the hierarchy only for an operation that caches integration-point data across many evaluations (8.3 may be it). Design pass, no code until an operation asks. S
  • 3.4.8 Format code that drifted apart. The Nastran writers share detail/nastran_write_common; their card tables and unique-id rules differ by design. What remains: _read_single_mesh (src/python/meshioplusplus/med/_medmulti.py lines 373–480) repeats read of med/_med.py (line 375): points (397), the FAS lookup (402–413), cells including the POG block (415–435). Extract _read_mesh_group(mesh_group, fas) in _med.py, called by both. S
  • 3.4.9 Binding and server boilerplate. Mostly closed: _forward (mcp/_server.py line 54) wraps 193 of the 201 tools. Nine keep hand-written bodies because they transform an argument first: sequence (362), render_mesh (2347), convert_coordinates (3049), estimate_normals (3271), extrude (3728), revolve (3766), mirror (3803), remesh (4240), screenshot (5089): leave them. The C API has 161 fail(MIO_ERR_INVALID_ARG, …) null checks (bindings/c/c_api.cpp, fail at line 426); a capi_require_arg(ptr, "name") macro returning the status would share them without changing a code. S
  • 3.4.11 Copies kept for the instruction gate. Shares measured and undone, to reopen only with a form that leaves the callers' code generation alone, measured with tools/bench_instructions.py: the point-field validation of gradient (gradient.cpp lines 502–557) and hessian (hessian.cpp 54–85), where one call in place of the inline throws cost gradient_gg 0.7% and hessian 1.2%; one sign-and-digits scan under the four integer parsers of detail/text_cursor.hpp (strtoll_token 351, strtoull_token 380, ExtractInt, ExtractUnsigned: read_ugrid +8.1%, read_gmsh22_ascii +6.2%, though three readers ran 5 to 6% faster); and GeometryUtils::TriangleArea for quality's tri_area lambda (quality.cpp 325, +1.8%). Already reopened with MIO_ALWAYS_INLINE and shared: detail/staged_block.hpp, ccells_finish_identity (convert_cells.cpp 379), detail/subset_gather.hpp. First thing to try on the rest: the same macro. S
  • 3.4.12 Behaviour the copies disagreed on. Decide each, then make the copies agree, with a test that pins the decision. S
    • time_values on a file object: nastran_op2/__init__.py (line 41), radioss_th (37), marc (65), ansys_rst (72) and lsdyna_d3plot (40) call the core unguarded; femap (37) and abaqus_fil (36) test is_buffer first. Recommended: guard all, through one helper in _shims.py.
    • _medmulti.py line 303 passes write_num=False to _write_mesh_step (_med.py 742), so med:num is never written by the multi-mesh writer. Recommended: write it.
    • _clean.py (259) and _crop.py (331) call _set_remap.remap_sets, which raises on an out-of-range set index; _decimate.py (652–673) filters and drops it. Recommended: raise everywhere; a set index out of range is a broken input.
    • Medit Tangents/TangentAtVertices: the core skips n tokens (formats/medit.cpp 570–579), the twin n lines with a warning (medit/_medit.py 497–505). Closes with 2.3.9.5, which reads them.

3.5 Clang-Tidy beyond includes ​

Grow the checked set in stages, each first an advisory report and then a gate on new findings. M–L

  • State today. .clang-tidy is Checks: '-*,misc-include-cleaner' with a HeaderFilterRegex over src/cpp/(include|src)|bindings/c and no naming options. tools/performance_tidy.py (307 lines, behind tools/performance-tidy.sh) configures build/performance-tidy-<backend> with Clang, CMAKE_EXPORT_COMPILE_COMMANDS=ON, SEQ, the C API and CLI on and Python off, runs the checks per translation unit and writes deduplicated JSON (schema_version 1); but CHECKS = "-*,performance-*" is hard-coded at line 17 and the check-list regex at line 239 matches performance- only. No baseline file is committed: the counts live in benchmarks (520, 283 and 284 on MESHIO, NATIVE and KRATOS after the first batch). tools/include-cleanup.sh and the include-cleanup job (ci.yml lines 1360–1383) are the gate pattern. clang-tidy-diff ships with the runner's clang-tidy.
  • Steps.
    • 3.5.1 A general runner. Rename to tools/tidy_report.py with --checks and --baseline FILE (compare by file, check and message, not line), keep performance-tidy.sh as a wrapper, and commit a baseline per family under tools/tidy/. Test in tests/python/ beside test_check_abi_version.py.
    • 3.5.2 bugprone-* first (the family most likely to find defects), then cert-* and misc-* beyond includes: report, triage into fixed, NOLINT(<check>): <reason> and disabled-with-reason in .clang-tidy, then gate.
    • 3.5.3 A chosen subset of cppcoreguidelines-* (init-variables, slicing, narrowing-conversions on the readers, pro-type-member-init); not the ownership or bounds families, which fight the NDArray views.
    • 3.5.4 modernize-* and readability-identifier-naming configured for the house style (CamelCase types and methods, m prefix members, r/p prefix parameters, lower_case free functions and locals), with the Python and JS API string names untouched.
    • 3.5.5 The gate. A tidy-diff step after include-cleanup: git diff -U0 origin/master... | clang-tidy-diff -p1 -path <build> over the enabled set, failing on any finding in changed lines; the full report runs on master beside SonarCloud.
    • 3.5.6 Triage the overlap with SonarCloud's C++ rules and 3.10 once (a table in static analysis: rule family, owning tool), and close the performance-* inventory.
  • ABI. A fix in an installed header follows the ABI policy: an inline-body change is Tier B.
  • Done when. .clang-tidy enables the four families' chosen checks and the diff gate is a required check.

3.6 Compiler warnings ​

The C++ build sets no warning flag on any compiler, so what every compiler finds for free is never seen. M–L

  • State today. No -Wall, -Wextra, /W4 or COMPILE_WARNING_AS_ERROR in CMakeLists.txt or cmake/; the only -Wall -Wextra is bindings/octave/meshioplusplus/tools/dev_build.sh line 39. The core's flags accumulate in _mio_core_copts (lines 746–752) and are applied PUBLIC by meshioplusplus_configure_core_target() (lines 1412–1455), so they reach the exported INTERFACE_COMPILE_OPTIONS of the installed targets: MESHIOPLUSPLUS_COVERAGE (819–827) and MESHIOPLUSPLUS_SANITIZE (843–879) use that list on purpose, a warnings option must not. Third-party code compiled into the core's own source list: pugixml.cpp (line 708) and the gidpost C sources (716); the Eigen, JSON and Polyscope submodules are under src/cpp/third_party/.
  • Steps.
    • 3.6.1 The option. option(MESHIOPLUSPLUS_WARNINGS "…" OFF) and MESHIOPLUSPLUS_WARNINGS_AS_ERRORS, with a function meshioplusplus_target_warnings(target) that adds the flags PRIVATE (-Wall -Wextra on GNU and Clang, /W4 on MSVC) and is called for meshioplusplus_core_obj, each meshioplusplus_core_<backend>, _core, meshioplusplus_c_obj, meshioplusplus_cli, meshioplusplus_tests and the benchmarks. Exempt the vendored sources with set_source_files_properties(... PROPERTIES COMPILE_OPTIONS -w) and mark third-party include directories SYSTEM (the _mio_core_sys_incs list exists). A --warnings switch in build/configure.sh and .bat. A test in the consumer project asserts the installed target carries no -W option.
    • 3.6.2 The count. One advisory CI step per compiler that builds with the option and uploads the log; record the three counts here.
    • 3.6.3 To zero, then -Werror. Fix by family, readers first; then MESHIOPLUSPLUS_WARNINGS_AS_ERRORS=ON in one cpp-tests leg (GCC), cpp-sanitize (Clang) and one Windows leg.
    • 3.6.4 The warnings that find defects in parsing code, one at a time, each to zero before the next: -Wshadow, -Wconversion and -Wsign-conversion on src/cpp/src/formats/ and detail/parse_guard.hpp first (a narrowed header count is how a reader overflows), -Wold-style-cast, -Wnull-dereference, -Wimplicit-fallthrough, -Wformat=2.
    • 3.6.5 Warning-free headers for consumers. tests/consumer and the amalgamation job's smoke compile build with -Wall -Wextra -Werror (and /W4 /WX), so a user's strict build does not break on meshio++'s installed headers or the single header.
  • Done when. The three -Werror legs are green and the consumer test compiles the headers at that level.

3.7 Typed Python ​

A user's mypy or pyright sees every call as Any, and an editor completes nothing from the extension. M–L

  • State today. No py.typed, no .pyi, no type checker in pyproject.toml, tox.ini, .pre-commit-config.yaml or CI. Annotated definitions are few (_helpers.py 19 of 28, _mesh.py 7 of 22). __all__ is a static list (src/python/meshioplusplus/__init__.py line 631, about 357 names) resolved lazily through _LAZY_ATTRS (376) and a module __getattr__ (992–1013), with the imports mirrored under TYPE_CHECKING, so a checker can follow them. _core is one pybind11 module (bindings/python/_core.cpp, 10,482 lines). The MCP tool functions (mcp/_tools.py) have no annotations; the typed wrappers are in mcp/_server.py.
  • Steps.
    • 3.7.1 Stubs for _core. Generate src/python/meshioplusplus/_core.pyi with pybind11-stubgen from a built extension (tools/gen_core_stubs.sh), commit it, and add a freshness check to the test job in the manner of tools/amalgamate.sh --check; fix the pybind signatures that render as raw C++ types (missing py::arg names, unregistered types) until stubgen reports none.
    • 3.7.2 py.typed. An empty marker in the package (it ships because wheel.packages covers the directory; assert it in tests/wheel_base_smoke.py).
    • 3.7.3 A checker, staged. [tool.mypy] in pyproject.toml (or pyright; pick one), a typecheck tox environment and CI job: first __init__.py, _mesh.py, _helpers.py, _fallback.py, _shims.py under --strict; then the operation shims (_<op>.py); then the CLI and MCP packages; the format twins last and non-strict. Each stage advisory, then required.
    • 3.7.4 A typed-usage test. tests/python/typing/usage.py (a script that reads, cleans and writes) checked with mypy --strict in the job: the probe that a user's code type-checks.
    • 3.7.5 Ruff, measured. Replace isort, black and flake8 in .pre-commit-config.yaml, tox.ini and the justfile with Ruff only if its formatter leaves the tree unchanged or the one-commit diff is reviewable (record it in .git-blame-ignore-revs); add B, UP, SIM, PERF and S advisory first. Whether or not Ruff lands, fix the hook: .flake8 selects B and B9, but the flake8 hook has no additional_dependencies: [flake8-bugbear], so those checks have never run (nor in tox's lint).
  • Done when. mypy --strict tests/python/typing/usage.py passes against an installed wheel.

3.8 clang-format as a check ​

  • State today. .clang-format exists (BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 100, SortIncludes: Never) and AGENTS.md asks for it; no hook or job runs it.
  • Steps. (1) Measure: git ls-files 'src/cpp/**' 'bindings/**/*.{cpp,hpp,h,c}' | xargs clang-format --dry-run at one pinned version, excluding src/cpp/third_party, the single header and the generated tables; record the changed-line count. (2) If small: reformat in one commit listed in .git-blame-ignore-revs, regenerate the single header, confirm tools/bench_ops.sh --hash is unchanged (formatting cannot move a digest; the check is that nothing else slipped in). If large: skip the reformat. (3) Either way, gate changed lines: a pre-commit hook (pre-commit/mirrors-clang-format, pinned) and a lint step running git clang-format --diff origin/master.
  • Done when. A pull request with a mis-indented line fails lint. S

3.9 More sanitizers and hardened runs ​

  • State today. cpp-sanitize (ci.yml line 238) runs ASan and UBSan with Clang on the SEQ backend without Python. MESHIOPLUSPLUS_SANITIZE takes any list, but CMake stops with a fatal error when it is combined with Emscripten or MESHIOPLUSPLUS_BUILD_PYTHON (CMakeLists.txt lines 868–871), and build/configure.sh forces Python off (lines 177–180). The parallel backends are in include/meshioplusplus/parallel.hpp; the THREADS pool, new in v19.0.0, is src/cpp/src/parallel_threads.cpp. Parallel tests: suites Parallel, ParallelThreads, LibraryLock, and tests/cpp/thread_sweep.hpp in 20 test files.
  • Steps.
    • 3.9.1 ThreadSanitizer. A cpp-tsan job: Clang, -DMESHIOPLUSPLUS_SANITIZE=thread, -DMESHIOPLUSPLUS_PARALLEL_BACKEND=THREADS, then TBB, tests with MESHIOPLUSPLUS_NUM_THREADS=4; OpenMP needs an instrumented libomp (the libomp built with -DLIBOMP_TSAN_SUPPORT=ON, or archer) and comes last. The determinism job compares outputs, which a benign-looking race passes; this is the first race detector on the pool. Suppressions in tools/sanitizers/tsan.supp, each with its reason. M
    • 3.9.2 The Python extension under ASan. Lift the CMake guard behind an explicit MESHIOPLUSPLUS_SANITIZE_PYTHON=ON; a job that builds _core with ASan and runs tests/python (the fast subset) with LD_PRELOAD=$(clang -print-file-name=libclang_rt.asan-x86_64.so) and ASAN_OPTIONS=detect_leaks=0. Target: bindings/python/np_conversions.hpp, where capsule-backed arrays and borrowed views cross the language boundary and no sanitizer has looked. M
    • 3.9.3 Hardened standard libraries. One cpp-tests leg with -D_GLIBCXX_ASSERTIONS and one Clang leg with -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE and -stdlib=libc++: an out-of-range operator[] becomes an abort. Through a MESHIOPLUSPLUS_HARDENED_STL option applied PRIVATE like 3.6's. S
    • 3.9.4 MemorySanitizer on meshioplusplus_fuzz_read for the formats that need no external library (everything outside tools/fuzz/library_formats.txt), with an MSan-instrumented libc++; advisory. M
  • Done when. cpp-tsan is a required check on THREADS and TBB.

3.10 More analyzers, triaged once ​

Nothing analyses a pull request before it merges: SonarCloud scans master only, with continue-on-error. M

  • Steps.
    • 3.10.1 CodeQL. .github/workflows/codeql.yml on pull requests and weekly: cpp (build mode manual, the cpp-tests configure line with SEQ and Python off), python, javascript-typescript (src/viewer, src/wasm) and actions. Triage the first run; queries closed by design go in a codeql-config.yml with reasons, mirroring sonar.issue.ignore.multicriteria. Its taint tracking covers what Sonar flagged by hand: the MCP server's _resolve (mcp/_tools.py line 325) and the HTTP companion.
    • 3.10.2 GCC -fanalyzer and Clang's analyzer, advisory: -fanalyzer on bindings/c/c_api.cpp and src/cpp/src/formats/ in one scheduled job (it is slow and noisy on C++; keep it off pull requests), and the clang-analyzer-* checks through 3.5.1's runner.
    • 3.10.3 Thread-safety annotations. A detail/thread_annotations.hpp with the Clang capability macros (empty elsewhere) and -Wthread-safety in the Clang legs, on the mutexes the core owns: library_mutex() (src/cpp/src/detail/library_lock.cpp lines 26–33, with LibraryLock/LibraryUnlock), ProvidersState::mMutex and plugin_scan_mutex() (src/cpp/src/providers.cpp lines 88 and 294), the pool's mSubmit, mMutex and gPthrCreate (parallel_threads.cpp lines 193–206), buffer_allocator_mutex() (the installed ndarray.hpp line 78: a comment-only change there, no ABI tier), log_mutex (log.hpp 177), mmg_mutex(), netgen_mutex() and the cache mutex of formats/nastran_op2.cpp (4302). This makes "no two threads inside the libraries" (5.8.2) a compile-time property.
    • 3.10.4 One owner per rule family, decided with 3.5.6 and recorded in static analysis.
  • Done when. CodeQL is a required check with zero open alerts of severity high.

3.11 Coverage and test depth ​

  • 3.11.1 Coverage on pull requests. The coverage job (ci.yml lines 1534–1745) runs only on pushes to master and tags (line 1537); .codecov.yml sets patch: false and project threshold: 1%. Run it on pull requests (it uses ccache already, lines 1616–1620), set patch: { default: { target: 80%, threshold: 5% } }, and keep the coverage-reports artifact SonarCloud consumes (7.2). S
  • 3.11.2 Hypothesis everywhere. tests/python/test_properties.py (ten properties) is importorskip-ed and Hypothesis is installed at ci.yml line 1611 only: add it to the test job's install line with a CI profile (derandomize=True, a deadline that fits the slowest runner). S
  • 3.11.3 A mutation-testing spike. On the shared kernels (math_utils.hpp, geometry_utils.hpp, fem_kernels.hpp, detail/parse_guard.hpp) with Mull against meshioplusplus_tests, and on _helpers.py and one format twin with mutmut: the surviving mutants name covered lines no assertion checks. Record the score and the survivors in static analysis, fix the tests, then decide whether a monthly job is worth its minutes. S–M
  • 3.11.4 Examples that run. No job executes the notebooks (72 in example/python, 11 in example/cpp, 4 each for Julia, R and Octave): a weekly notebooks job runs PYVISTA_OFF_SCREEN=true jupyter nbconvert --to notebook --execute on the Python ones into a scratch directory and fails on an exception (outputs are not compared). And pytest-codeblocks covers tests/python/ only (ci.yml lines 1513 and 1662): add pytest --codeblocks README.md doc/ to the test job, marking the blocks that need optional packages or files with <!--pytest-codeblocks:skip-->. S–M

3.12 Fuzzing beyond readers ​

After 3.1, so new targets run where the old ones do. M

  • State today. tests/fuzz/fuzz_read.cpp reads every registry format (through a scratch file: the registry is path-only, registry.hpp has no in-memory read or write); fuzz_mesh_cad.cpp holds the two operation targets, dispatched by MIO_FUZZ_TARGET or the binary name and reporting through fuzz_op_finding; hdf5_mutator.cpp is the structure-aware HDF5 mutator. Targets are defined at CMakeLists.txt lines 2144–2192 and regressions become fuzz_regression_<dir> tests at 2193–2219 (op_* directories replay through meshioplusplus_fuzz_ops_replay). fuzz.yml runs weekly and on pull requests that touch the fuzz paths.
  • Steps.
    • 3.12.1 A round-trip target in tests/fuzz/fuzz_roundtrip.cpp: read the input as format F; if it reads, write it to a scratch path with registry_writers().at(F), read that, and call fuzz_op_finding when the second read throws or differs from the first beyond the format's declared losses (compare counts, cell types and array names and shapes first; values only for the formats conformance_spec.py declares exact). Finds writer crashes and reader/writer disagreements no reader target can.
    • 3.12.2 A differential replay, not a libFuzzer target: tools/fuzz/diff_engines.py replays each text format's corpus through _core (MESHIOPLUSPLUS_STRICT_CORE=1) and the Python twin and reports inputs where one reads and the other raises, or the meshes differ. Weekly, in fuzz.yml, after the campaign, on its corpus.
    • 3.12.3 The C API as tests/fuzz/fuzz_c_api.cpp: a byte-driven sequence of mio_mesh_* builders, getters after mutation (borrow windows), option structs with random reserved tails, and mio_read on the remaining bytes; any crash or a non-status failure is a finding.
    • 3.12.4 Operations on untrusted meshes: clean, repair, convert_cells, decimate, extract_surface, tetrahedralize's native backend, each taking the mesh from the reader target's corpus for a compact format (off, medit) so the fuzzer mutates meshes, with the allowed exceptions listed per operation as fuzz_mesh_cad.cpp does.
    • 3.12.5 Parsers of text the user writes: the pipeline JSON (pipe_document_from_json) and the native CLI's argument vector.
    • Register each in CMakeLists.txt, tools/fuzz/run_campaign.sh, the OSS-Fuzz build.sh and fuzzing.
  • Done when. The weekly campaign runs the new targets and their first findings are fixed with regressions under tests/fuzz/regressions/.

3.13 A mechanical ABI and API diff ​

  • State today. tools/check-abi-version.sh fails when an installed header changed since the last tag without a version bump or an ABI review row naming it; tests/cpp/test_abi_layout.cpp pins 195 types, on Linux x86-64 with libstdc++ only; a link-time sentinel covers version mismatch. Nothing sees an exported symbol that changed or disappeared, or a type nobody listed. The shared libraries are meshioplusplus (C API, SOVERSION 0, CMakeLists.txt lines 1928–1936) and meshioplusplus_core_<backend> (SOVERSION = the ABI version, shared with BUILD_SHARED_LIBS or MESHIOPLUSPLUS_PYTHON_SHARED_CORE).
  • Steps.
    • 3.13.1 abidiff on the C API, gating. In cpp-install: build the C library at the last release tag and at head with -g, run libabigail's abidiff --headers-dir1/2 bindings/c/include; any removed or changed symbol or struct layout fails unless the major version changed. The C promise is the strict one, so this is a gate from day one. S
    • 3.13.2 abidiff on the shared core, advisory, with a suppression file for detail:: and with the report attached to the pull request: a changed type with no ABI bump is what the review row should have caught. S
    • 3.13.3 A Python API diff. A committed tools/api/python_api.json (each name of __all__ with its inspect.signature and defaults, generated by tools/gen_api_dump.py), a freshness test, and a rule in lint: a removed name or a changed default in the diff needs a **Breaking:** line in CHANGELOG.md. The MCP parity test can read the same file. S–M
  • Done when. Removing a C function in a test branch fails cpp-install.

3.14 Supply chain ​

Publishing already uses OIDC for PyPI and (with a token fallback, wasm.yml lines 278–307) npm, and downloads are hash-pinned (build/deps-pins.sh). Each sub-item is S.

  • 3.14.1 SECURITY.md at the root: supported versions (the last minor), private reporting through GitHub's security advisories, what is in scope (readers on untrusted files, the MCP server's sandbox, the HTTP companion), the response target. Link it from README.md and fuzzing. Before 3.1.4.
  • 3.14.2 Dependency updates. .github/dependabot.yml for github-actions, npm in src/viewer, src/wasm and doc, and pip for the pinned tools; grouped weekly. The C libraries of build/deps-pins.sh are not covered by it: a scheduled job that compares each pin with its upstream's latest release and opens an issue.
  • 3.14.3 Actions pinned by commit. Every uses: is a tag, and pypa/gh-action-pypi-publish@release/v1 a branch: pin each to a SHA with the tag in a comment (Dependabot keeps them current), and add permissions: contents: read at the top of each workflow with per-job elevation.
  • 3.14.4 OpenSSF Scorecard workflow with the badge in README.md; it then tracks the rest.
  • 3.14.5 Provenance. actions/attest-build-provenance for the wheels and sdist (wheels.yml), the CLI archives (cli.yml), the WASM tarball (wasm.yml) and the Conan and vcpkg artifacts; verification (gh attestation verify) documented in installation. Drop npm's token fallback once OIDC has published a release.
  • 3.14.6 An SBOM. CycloneDX JSON per wheel and per CLI archive, generated from build/deps-pins.sh, build/occt-recipe.sh and the CMake cache (a tools/gen_sbom.py), naming the statically linked HDF5, netCDF, zlib, zstd, LZ4, bzip2, CGNS, KaHIP, CutCells and OpenCASCADE versions; a test that THIRD_PARTY_NOTICES.md's version column agrees with the pins, which today is kept by hand.
  • 3.14.7 Licence identifiers. 34 sources carry SPDX-License-Identifier: MIT; the rest an ASCII banner. Add the identifier to the banner everywhere (a scripted, comment-only change; regenerate the single header) and a REUSE.toml covering fixtures and generated files, with reuse lint in lint.

3.15 The platform matrix ​

  • State today. ubuntu-24.04's GCC 13, an unversioned apt Clang, Apple Clang, MSVC; gcc-toolset-13 in the manylinux_2_34 images; CPython 3.10 to 3.14 with the GIL; CIBW_SKIP: "*-musllinux* *_i686 *-win32" (wheels.yml lines 108–109). No minimum compiler is stated anywhere but the Spack recipe's conflicts below GCC 10; wheel comments say <format> needs GCC 13.
  • Steps.
    • 3.15.1 Free-threaded CPython. (a) Raise the pybind11 floor from >=2.11 (pyproject.toml line 2) to a version with py::mod_gil_not_used (2.13 or later) and replace the hand-rolled once-caches the comment at bindings/python/np_conversions.hpp line 172 explains. (b) Audit the state the bindings share: the leaked class caches sMesh, sRegion, sRows, sFaces (np_conversions.hpp lines 177–200, set "under the GIL"), _ALL_LOADED and the three _Registry dicts in _helpers.py (lines 61 and 113–115), _strict in _fallback.py, the module __getattr__ writing globals(), the unlocked caches in nastran_op2/_write.py (1795), nastran_h5/_write.py (98) and _tessellation.py (907), _LIB in szplt/_szplt.py. (c) Declare the module (PYBIND11_MODULE(_core, m, py::mod_gil_not_used()), _core.cpp line 1998) only when 3.9.1's ThreadSanitizer run of a threaded pytest subset is clean. (d) Add cp313t-* cp314t-* to CIBW_BUILD (the patterns cp313-* do not match them) and a 3.14t entry to the test matrix. Without the wheel a free-threaded interpreter re-enables the GIL on import, with a warning. M–L
    • 3.15.2 musllinux. Drop *-musllinux* from CIBW_SKIP for the base wheel; build/build-permissive-libs.sh must build under musl (HDF5 and netCDF do); -static-libstdc++ works there. Add-ons after the base. S–M
    • 3.15.3 Big-endian. A weekly s390x job under QEMU (uraimo/run-on-arch-action or a --platform linux/s390x container): configure with SEQ, HDF5 and netCDF off, run meshioplusplus_tests. Every binary reader and writer states a byte order through detail/byteswap.hpp; nothing has run them where the host's differs. Expect findings: list them in §1 as they appear. S–M
    • 3.15.4 Stated compilers. Decide the floor by building: GCC 11, 12, 13 and Clang 15 to 17 in a compiler-matrix job (containers); state the lowest that passes in installation and README.md, keep that leg required, and add the newest GCC and Clang as an advisory leg. S
    • 3.15.5 PyPy. Not queued: pybind11 supports it, no user has asked.
  • Done when. pip install meshioplusplus on python3.14t and on Alpine installs a wheel, and the s390x job is green.

3.16 Documentation checks ​

  • 3.16.1 Links. lychee over doc/.vitepress/dist and README.md: internal links and anchors on pull requests (in docs.yml, which has no check today), external ones weekly with an allow-list for vendor portals that block bots. S
  • 3.16.2 This file checks itself. tests/python/test_roadmap.py, beside test_doc_diagrams.py: every N.M or N.M.K cited in running text names a heading or a bold item that exists (the November 2026 passes found ten left behind by renumberings); every item in Suggested sequencing and every box of roadmap_map() (doc/diagrams/figures/flows.py) is open; every path in backticks that looks like a repository path exists. The same test should cover the references to roadmap numbers in code and other pages, which are stale today and to be corrected with it: doc/benchmarks.md (lines 931, 1007, 1257, 1271), src/cpp/src/detail/always_inline.hpp, src/cpp/src/detail/library_lock.hpp, src/cpp/src/operations/optimize_volume.cpp, src/cpp/benchmark/bench_ops.cpp (line 1066) and .github/workflows/wheels.yml (line 139) cite numbers of earlier layouts; replace each number with the item's anchor or title. S
  • 3.16.3 Spelling. codespell in pre-commit with a project word list. S
  • 3.16.4 Claims the tree does not back. Fuzzing line 3 lists a VS Code extension among the consumers of untrusted files; only .vscode/tasks.json exists (remove it, or build 4.13). MCP says 126 tools where TOOL_REGISTRY holds 203: generate the count, as the conformance table is generated. The comment at CMakeLists.txt line 1619 says "All 112 headers" of 283. S

3.17 Build ergonomics ​

  • 3.17.1 CMake presets. A CMakePresets.json (schema 3, which CMake 3.21 reads; the project's floor of 3.15 is unaffected because presets are optional) with configure, build and test presets: dev (tests, STL backend), tests-<backend>, sanitize, fuzzers, coverage, lto, bench, cli, c-api, install-cpp, wasm (toolchain from EMSDK). build/configure.sh has no switch for coverage, LTO, benchmarks or JSON, so the presets set those variables directly and the script gains --preset; CI's hand-written configure lines move to presets one job at a time. S–M
  • 3.17.2 A compiler cache in every job. ccache appears in coverage only (ci.yml lines 1616–1620, hendrikmuhs/ccache-action, launcher flags at 1632–1633). Add it to cpp-tests, cpp-sanitize, determinism, instructions, cpp-install and the optional-dependency jobs, keyed by job and compiler; sccache on Windows. Not the instructions build if it changes counts (it cannot, but verify once). S
  • 3.17.3 Compile time. -ftime-trace with ClangBuildAnalyzer on a full build; report the ten slowest translation units (src/cpp/cli/main.cpp, bindings/python/_core.cpp at 10,482 lines, bindings/c/c_api.cpp, bindings/wasm/js_bindings.cpp at 11,083 lines) and headers in benchmarks. Then, by what it shows: split _core.cpp and js_bindings.cpp by area into several sources (they are globbed, so no CMake change), precompiled headers for meshioplusplus_tests, and a CMAKE_UNITY_BUILD=ON leg as a check that the amalgamation's unique-name rule holds for the sources too. M
  • 3.17.4 Hardening the released binaries. For cli.yml and wheels.yml builds only, through a MESHIOPLUSPLUS_HARDENING option applied PRIVATE: -D_FORTIFY_SOURCE=3, -fstack-protector-strong, -Wl,-z,relro,-z,now, -fcf-protection=full (x86-64) or -mbranch-protection=standard (arm64), /guard:cf and /Qspectre on MSVC. Measure on the instruction-count gate first and record the cost. S–M

4. Ecosystem reach ​

Admission: getting what exists to the people who would use it.

4.1 Registration and distribution ​

Mostly logistics, and all blocking real adoption; the effort is small per target but the calendar time is set by each registry's review, so start early. Two rows are not logistics, and say so. S each, calendar-bound, except 4.1.2 and 4.1.3.

ItemTargetStatus today
4.1.1conda-forgeNo recipe exists.
4.1.2CRANNot submittable as it stands: the package needs a C library CRAN's builders do not have. M
4.1.3Julia General, with a JLLNot registered; no binary package, so Pkg.add would give a package that cannot load its library. M
4.1.4Blender Extensions PlatformZips built and attached to every release; only the listing remains.
4.1.5ConanCenter, the vcpkg registryRecipes are CI-validated but in self-hosted shape, not upstream shape.
4.1.6A ParaView plugin indexBundles ship on every release and load in Kitware's binaries; only a listing remains.
4.1.7Homebrew, NixNot started.
4.1.8A container imageNot started.
4.1.9Octave Packages index, MATLAB File ExchangeNot started.

Spack is upstream already (meshioplusplus and py-meshioplusplus in spack/spack-packages), and its bump is part of the release checklist in AGENTS.md.

  • 4.1.1 conda-forge. A staged-recipes pull request with one recipe, two outputs (libmeshioplusplus: the C and C++ libraries and the native CLI; meshioplusplus: the Python package). Source: the PyPI sdist (a GitHub tarball lacks the Eigen, JSON and Polyscope submodules). Host dependencies as the paraview-plugin job already installs them from conda-forge (ci.yml near lines 1900–1915): hdf5, libnetcdf, zlib, zstd, lz4-c, bzip2, cgns, kahip, pybind11, scikit-build-core, numpy, a C++20 compiler. CMake: -DMESHIOPLUSPLUS_PREFER_STATIC_DEPS=OFF, the WITH_* options on for what conda-forge has. OpenCASCADE is on by default (CMakeLists.txt line 435) and conda-forge has occt: decide between an MIT base output with -DMESHIOPLUSPLUS_WITH_OCCT=OFF plus a meshioplusplus-occt output (the PyPI split, recommended) and one LGPL-linked package. macOS deployment target 13.3. Probe: conda build locally, then conda create -n t meshioplusplus && python -c "import meshioplusplus".
  • 4.1.2 CRAN. bindings/r/meshioplusplus/DESCRIPTION says SystemRequirements: meshio++ C library (libmeshioplusplus), C99, and configure finds an installed library (MESHIOPLUSPLUS_CFLAGS/LIBS, MESHIOPLUSPLUS_HOME, then pkg-config) or exits; R CMD check --as-cran passes in CI only because the r job builds the C API first (ci.yml lines 2640–2663). CRAN's machines have no libmeshioplusplus, so the package would fail there. Options, to decide first: (a) vendor the core's sources into the package tarball (a tools/make_cran_tarball.sh copying src/cpp/include, src/cpp/src and bindings/c under src/, a Makevars that compiles them with CXX_STD = CXX20, HDF5 and netCDF off unless found), within CRAN's 5 MB tarball limit and its compile-time limit, which the core's size may exceed: measure both before anything else; (b) a configure that downloads a prebuilt static library per platform from the GitHub release, which CRAN tolerates for Windows and macOS binaries but not as the only path on Linux; (c) R-universe instead of CRAN, which builds from the repository with system requirements installable, and needs only a packages.json. Recommended: (c) now, (a) if the measurements allow. Also missing: cran-comments.md, a cleanup script, inst/COPYRIGHTS for vendored code. Probe: R CMD check --as-cran in a container with no meshio++ installed.
  • 4.1.3 Julia. bindings/julia/MeshioPlusPlus/Project.toml has [compat] julia = "1.9" only, and src/libmio.jl loads ENV["MESHIOPLUSPLUS_LIB"] or whatever Libdl.find_library finds (lines 35–53); Julia says "deliberately no JLL". Steps: (1) a build_tarballs.jl for Yggdrasil building the C library (-DMESHIOPLUSPLUS_BUILD_C_API=ON, Python off, HDF5 and netCDF from HDF5_jll and NetCDF_jll) for the platforms BinaryBuilder supports with C++20, giving meshioplusplus_jll; (2) libmio.jl prefers the JLL's libmeshioplusplus and keeps the environment override, with meshioplusplus_jll in [deps] and [compat]; (3) register through Registrator with subdir=bindings/julia/MeshioPlusPlus and TagBot for tags. The binding is GPL-3.0 (bindings/julia/LICENSE) while the library is MIT: confirm that is intended before the registration makes it permanent. Probe: Pkg.add("MeshioPlusPlus"); using MeshioPlusPlus; MeshioPlusPlus.version() in a clean depot.
  • 4.1.4 Blender. src/blender/blender_manifest.toml.in (id meshioplusplus, license ["SPDX:MIT"], blender_version_min 4.2.0), built per platform by wheels.yml's blender_extension job with cp311 wheels. Submit at extensions.blender.org; the review asks for the bundled wheels' licences (the base wheel is MIT with static permissive libraries: point to LICENSE-BINARIES.md). Add the listing to Blender.
  • 4.1.5 ConanCenter and vcpkg. Conan: the real recipe is the root conanfile.py, which uses exports_sources with local paths; packages/conan/ is the test package. ConanCenter needs recipes/meshioplusplus/all/{conanfile.py,conandata.yml,test_package/} with source() calling get(**self.conan_data["sources"][version]) on the release tarball and its sha256, and dependencies as ConanCenter requirements rather than vendored. vcpkg: packages/vcpkg/meshioplusplus/portfile.cmake has SHA512 0 and HEAD_REF main, and a private overlay port packages/vcpkg/libaec/; upstream needs the real hash, a versions/ entry (vcpkg x-add-version), and either the upstream libaec or a pull request fixing it there first. Both recipes force Eigen and JSON off, which is right for a registry build. Generate both upstream forms from the self-hosted ones in the release job, so they cannot drift.
  • 4.1.6 ParaView. tools/paraview-meshioplusplus-plugin.py with the bundles of src/paraview/build_bundle.py. There is no central index; the listing is a page on the ParaView Discourse "Plugins" category and an entry in Kitware's community-plugin list, with the install lines of ParaView plugin.
  • 4.1.7 Homebrew, Nix. A formula in a tap first (loumalouomega/homebrew-meshioplusplus: the native CLI and the C and C++ libraries, depends_on "hdf5", "netcdf", "cmake" => :build), homebrew-core once it has users; a flake.nix at the repository root exposing the CLI and the Python package, then nixpkgs. The release binaries cover the CLI meanwhile.
  • 4.1.8 A container image. ghcr.io/loumalouomega/meshioplusplus:<version>: a slim Python base with pip install "meshioplusplus[all,addons]" and the native CLI archive, built and pushed by a job in wheels.yml after publishing, with 3.14.5's attestation; for CI pipelines and HPC (Apptainer pulls it). Only the OSS-Fuzz Dockerfile exists today.
  • 4.1.9 Octave and MATLAB. pkg install takes a tarball built by hand (Octave / MATLAB). Attach meshioplusplus-octave-<version>.tar.gz to the release (a step in wheels.yml: tar -C bindings/octave -czf …), then a pull request to gnu-octave/packages with the package's YAML. MATLAB itself has never been run in CI: add a matlab-actions/setup-matlab leg to the octave job running the same test directory before listing on File Exchange.

4.2 Rust bindings ​

Over the C API: the next language by scientific adoption after Julia and R. M

  • State today. No bindings/rust. The header bindings/c/include/meshioplusplus/meshioplusplus.h (9,181 lines; 639 functions, about 163 structs, about 30 enums) is C99, includes only stddef.h and stdint.h, and CI compiles a -std=c99 consumer. Conventions (header lines 25–40): a status or NULL/-1 return with the thread-local mio_last_error(); setters copy; getters borrow until the next mutation; row-major, 0-based; string getters follow snprintf. Discovery: meshioplusplus.pc and find_package(meshioplusplus).
  • Design.
    • meshioplusplus-sys: bindgen at build time from the installed header (pkg-config crate; MESHIOPLUSPLUS_DIR override), with care for: MIO_API needing MIO_SHARED on Windows, which the .pc does not set; the MIO_CELL_TYPES(X) X-macro behind enum mio_cell_type (lines 178–208), which bindgen expands; the function-like MIO_VERSION_AT_LEAST, which it skips (re-express as a const fn); enums as newtype constants, since the C side may add values. A vendored feature that builds the C library with the cmake crate.
    • meshioplusplus: safe wrappers. Mesh owns a *mut mio_mesh and frees it in Drop; the borrow window is the lifetime system's job: getters return &[T] or ndarray::ArrayView tied to &self, and every mutating method takes &mut self, so "borrows expire on mutation" is checked at compile time. Option structs are built by *_opts_init then field assignment behind builder types, never by literal (the reserved tails must stay zero). Errors: a Result<T, Error> carrying the status and mio_last_error().
  • Steps. (1) bindings/rust/{meshioplusplus-sys,meshioplusplus} as a workspace, version in step with the others (a row in AGENTS.md's version table). (2) Tests ported from the Julia suite's sections (bindings/julia/MeshioPlusPlus/test/runtests.jl: introspection, shape identity, the borrow window, file round trip, regions, each operation, errors carrying mio_last_error(), the XDMF series). (3) A parity guard, which no binding but Octave has today: tests/python/test_binding_parity.py parses the C header's function names and checks each is wrapped or listed as exempt, for Julia (ccall(_sym(:mio_…)) in src/*.jl), R and Rust alike. (4) A rust CI job in the pattern of julia (ci.yml lines 2488–2584: build the C API, run the tests against it, run the doc example, check that a missing library fails by name). (5) doc/rust.md on the template of doc/julia.md (building, array layout, the borrow window, memory, errors, documented gaps, tests), in the Language surfaces sidebar group (doc/.vitepress/config.mts line 253), and the binding lists in AGENTS.md. (6) crates.io publication from the release workflow.
  • Done when. cargo test passes against an installed C library on the three platforms, and the parity test covers every mio_ function.

4.3 Interop phase 2: Open3D and DOLFINx ​

  • State today. src/python/meshioplusplus/_interop.py: to_open3d (lines 1662–1668) and to_dolfinx (1671–1678) call _phase_two, which raises NotImplementedError; neither is in __all__; has_open3d and has_dolfinx (1025–1032) return a hard-coded False. The working bridges share a pure payload layer (_to_vtk_payload line 469, _to_triangles_payload 698, _to_table_payload 882; dataclasses VtkPayload, TrianglesPayload), _require(module, extra, op) (1035) for the install hint, and _copy_note/_emit for the copy report. Interop records the intended _to_tetra_payload and _to_dolfinx_payload with cell_perm_gmsh.
  • Steps.
    • 4.3.1 Open3D. to_open3d(mesh, kind="auto") from _to_triangles_payload to o3d.geometry.TriangleMesh (or PointCloud for a vertex-only mesh, TetraMesh through a new _to_tetra_payload), using the tensor API (o3d.t.geometry, which takes numpy without the Vector3dVector copy the stub's message cites) where present; from_open3d; colours and normals as point data. has_open3d becomes an import probe (_importable). An open3d extra. S
    • 4.3.2 DOLFINx. to_dolfinx(mesh, comm=MPI.COMM_SELF): one cell type only (refuse a mixed mesh by name), the coordinate element from basix.ufl.element("Lagrange", cell, degree, shape=(gdim,)), connectivity permuted from VTK to DOLFINx order with dolfinx.io.utils.cell_perm_vtk, dolfinx.mesh.create_mesh; cell regions to meshtags. from_dolfinx through mesh.geometry.x and mesh.geometry.dofmap. DOLFINx is conda- or apt-only, so no extra: the hint names both. Verify first against the current DOLFINx release's API, which has moved between minors. M
    • Both: export in __all__ with the lazy map (__init__.py lines 501–518), _NOT_TOOLS entries in tests/python/test_mcp.py (lines 2491–2511 hold the interop ones), gated tests in tests/python/test_interop.py in the pattern of the trimesh block (line 646), Open3D in the interop job's install line (ci.yml lines 1813–1847) and a DOLFINx leg in the conda-based paraview-plugin job's pattern, the Phase 2 section of the page, a notebook cell.
  • Done when. test_phase_two_targets_are_not_implemented (line 481) is deleted because both are.

4.4 A CLI chatbot ​

meshioplusplus chat, a natural-language entry point from the terminal. The MCP server already exposes the whole Python surface to an agent, but only to a host that speaks MCP; a user with an API key cannot ask the CLI "why does this file fail to convert" or "clean this mesh and tell me what changed". A thin client over existing work, not new mesh functionality. S–M

  • State today. TOOL_REGISTRY (src/python/meshioplusplus/mcp/_tools.py line 7665) maps 203 names to {"fn", "wraps", "gated"} with no schemas; call_tool(name, kwargs) (8517) dispatches and guard (8507) turns exceptions into {"error", "error_type"}; the sandbox is _resolve (325–349) under _ROOT (set_root, 269); results pass _sanitize/_json_safe (352–391). mcp/_http.py already dispatches /api/tools/{name} through call_tool (lines 200–220). Schemas exist only as the typed wrappers of mcp/_server.py, which need the mcp SDK to list.
  • 4.4.1 Reuse, not reimplementation. The loop calls _tools.call_tool in-process: never a second dispatch, sandbox or sanitizer. Every tool added to the registry reaches the chatbot, under the parity test_every_operation_has_a_tool gives.
  • 4.4.2 Tool schemas without the MCP SDK. The provider needs a JSON schema per tool, and the chat extra must not require mcp. Generate them from _server.py's wrapper signatures by introspection (inspect.signature and the annotations, the same data the SDK reads) in a small mcp/_schemas.py, with a test that it agrees with server.list_tools() when the SDK is installed (test_server_lists_every_registered_tool, tests/python/test_mcp.py line 3405, shows the listing). This is also the committed schema source 4.9.1 asks for.
  • 4.4.3 v1 scope. A stateless-per-turn loop (history and tool schemas out, tool calls executed, results fed back until a plain-text reply) over files under the working directory or --root (set_root); context lives in the terminal session only; a tool allow-list flag and a --read-only mode that withholds every tool whose wraps writes; each tool call echoed before it runs.
  • 4.4.4 Packaging. A chat extra carrying the provider's SDK, kept out of [all]; a new _cli/_chat.py with chat_cmd and one _Verb("chat", "_chat", "chat_cmd", …) row in _cli/_main.py's _VERBS; test_one_verb_imports_its_module_alone (tests/python/test_cli.py line 300) guards that nothing else imports it. Without the extra or a key the verb fails by name, as _require_server does in mcp/__init__.py. Tests drive the loop with a scripted fake provider. Provider-agnostic wiring and persisted history are follow-ups.
  • 4.4.5 Docs. doc/chat.md on the section layout of MCP (installation with the named error, running, the sandbox, what it can call), the sidebar, README.md, and the verb table of CLI.
  • Done when. meshioplusplus chat --root examples "what cell types does bracket.vtu hold?" answers by calling info, with a fake provider in CI and a real one by hand.

4.6 The dataset manager shows the stored thumbnail ​

  • State today. A manifest entry's Thumbnail is a PNG path the Python API writes and reads (src/python/meshioplusplus/_dataset.py: key at line 49, thumbnail_path 278, set_thumbnail 715, render_thumbnail 721). The browser manager (src/viewer/src/dataset/) parses and saves the key (manifest.ts lines 47 and 178–190) but shows no stored picture: entries are list chips (main.ts renderEntryList, lines 312–344), and the only image is the per-manifest overview card (overview.ts lines 229–233), fed by a session capture (thumbs.ts captureThumbnail, called from main.ts lines 598–607). A sandboxed file route already exists (GET /api/files?path=&token=, mcp/_http.py lines 222–239; ApiClient.fileUrl, api.ts lines 126–132), and a picked directory can serve bytes (WorkspaceFile.bytes(), fs.ts lines 25–31).
  • Steps. (1) A thumbnailUrl(entry) resolver: the companion's fileUrl when connected, else a Blob URL from the workspace file, else none. (2) Show it in the detail pane (renderDetail, main.ts 352–378) and as a small image on each chip, lazily (loading="lazy", revoke Blob URLs on re-render). (3) The overview card prefers the first entry's stored thumbnail over a session capture. (4) "Save preview as thumbnail": write the capture as PNG into the workspace and set the key (the File System Access path is readwrite already). (5) Tests: tests/viewer/dataset.spec.mjs (a manifest with a thumbnail shows an <img> with a non-empty naturalWidth) and tests/viewer/unit/manifest.test.mjs; update datasets, whose line 101 says the manager does not display it yet; rebuild the embedded page (7.3).
  • Done when. Opening a dataset written by render_thumbnail shows its pictures without opening any case. S

4.7 Split Python packages: Windows ​

v18.0.0 landed the provider registry, the shared core in the MIT base wheel, the meshioplusplus-{occt,netgen,mmg} add-ons, the permissive libraries linked static into the Linux and macOS base wheels, and plugin loading for the native CLI and the C API (plan). What is left is Windows. M

  • 4.7.1 The permissive libraries on Windows. build/build-permissive-libs.sh refuses MSYS and Cygwin (lines 30–36) and builds with -fPIC -fvisibility=hidden and make, so the Windows wheel sets WITH_HDF5=OFF WITH_NETCDF=OFF (wheels.yml lines 166–167) and Python falls back to h5py and netCDF4 (the __has_hdf5__-style flags, bindings/python/_core.cpp lines 2005–2057). Steps: a build/build-permissive-libs.ps1 (or a CMake ExternalProject superbuild that serves all three platforms from build/deps-pins.sh's versions and hashes) building zstd, LZ4, bzip2, HDF5, netCDF, CGNS and KaHIP static with /MD (the wheel's runtime; build-occt.sh --msvc-static-runtime and build-cutcells.sh are the MSVC precedents) and CMAKE_POLICY_DEFAULT_CMP0091=NEW; HDF5's symbols stay out of the core DLL's export table because cmake/gen_core_def.py exports the meshioplusplus namespace only; cache the prefix in the Windows leg as the other two do (wheels.yml lines 34–41); turn the options on in CIBW_ENVIRONMENT_WINDOWS. Probe: python -c "import meshioplusplus._core as c; assert c.__has_hdf5__" in the Windows wheel test, and MESHIOPLUSPLUS_STRICT_CORE=1 reading an XDMF file.
  • 4.7.2 Plugins on Windows. A DLL binds its imports at link time against a named import library, so a plugin cannot borrow its host's symbols as an ELF or Mach-O one does (ENABLE_EXPORTS, CMakeLists.txt line 2254; cmake/plugins.cmake stops with a fatal error on WIN32, lines 13–15). The wheel already solves it for Python: a shared meshioplusplus_core_meshio.dll with a generated export table (gen_core_def.py, wired at CMakeLists.txt lines 1572–1589) that the add-ons link. For the native CLI and the C API the same core DLL would be the host both link, which conflicts with the CLI's static runtime (/MT, lines 2279–2284; cli.yml builds OpenCASCADE with --msvc-static-runtime): exceptions and allocations must not cross a runtime boundary. Decision to take: a second Windows CLI flavour built /MD against the core DLL, shipped with it and able to load plugins, beside the single-file /MT executable that cannot. Then lift the WIN32 guard, export through src/cpp/providers/plugin_export.hpp, and enable Plugins.* (tests/cpp/test_plugins.cpp; today NotBuiltOnThisPlatform, line 123) on Windows.

4.8 Provider plugins beside the native CLI release ​

v18.0.0 lets the native CLI and the C API load providers from shared libraries (MESHIOPLUSPLUS_PLUGIN_PATH, providers and plugins), but no release ships one: MESHIOPLUSPLUS_BUILD_PLUGINS is off (CMakeLists.txt lines 1803–1807), no workflow sets it, and cli.yml packages the executable and its licence files only. Reopened from the MMG and Polyscope non-goals, which keep both out of the executable itself. M each

  • 4.8.1 MMG and Netgen plugins.
    • 4.8.1.1 The licence texts first. The plugin archive must carry the LGPL texts and relinking notes, and the add-on wheels it would copy them from do not have them: packages/python/meshioplusplus-{mmg,netgen} declare the licence in pyproject.toml and ship no license-files, no COPYING.LESSER and no relinking note (cmake/addon_common.cmake line 33 installs the target only). Fix the wheels in the same change: add the LGPL-3.0 and LGPL-2.1 texts and a RELINKING.md (the library is linked dynamically or its objects are provided; where to get the exact source, from build/deps-pins.sh) to both add-ons' license-files, as OpenCASCADE's texts already ship. A compliance gap in what is published today: take it before anything else in §4.
    • 4.8.1.2 Measure the risk. The executable links -static-libgcc -static-libstdc++ (CMakeLists.txt line 2278) with ENABLE_EXPORTS, a plugin needs the host's C++ runtime (providers.hpp), and no test loads a plugin into that configuration. In the manylinux container of cli.yml: build the fake plugin (tests/cpp/plugin/fake_plugin.cpp) and load it into the static executable; throw an exception across the boundary both ways. If it fails, the plugin-capable CLI is a second, dynamically linked flavour, as on Windows (4.7.2).
    • 4.8.1.3 Build and ship. -DMESHIOPLUSPLUS_BUILD_PLUGINS=ON with Netgen and MMG prefixes in the Linux and macOS legs of cli.yml, in the same container and toolchain; meshioplusplus_plugin_mmg and meshioplusplus_plugin_netgen (cmake/plugins.cmake lines 21–43) into meshioplusplus-cli-plugins-<version>-<os>.tar.gz with 4.8.1.1's files; MMG built without ELAS, as the wheel. Document in CLI and MMG backend.
    • Probe. In the release smoke step: MESHIOPLUSPLUS_PLUGIN_PATH=plugins ./meshioplusplus --version | grep -E "Netgen yes, MMG yes" (the line main.cpp prints at 10788–10805), and adapt on a cube exits 0. Windows waits on 4.7.2.
  • 4.8.2 Polyscope as a viewer plugin. The executable stays free of OpenGL, GLFW and X11.
    • State today. src/cpp/cli/polyscope_view.cpp (view_mesh line 252, screenshot_mesh 263) is linked PRIVATE when MESHIOPLUSPLUS_WITH_POLYSCOPE is on (CMakeLists.txt lines 2264–2275). Without it: view --backend polyscope reports the build flag (view_not_built, view_payload.cpp lines 638–644), view --backend auto falls back to the terminal viewer on a tty, and screenshot falls back to the software rasterizer and exits 0 (main.cpp lines 8536–8551).
    • Steps. A ViewerProvider in providers.hpp beside the four kinds there (mName, mView(const Mesh&, const ViewOptions&), mScreenshot(const Mesh&, const ViewOptions&, const std::string& path); named registration like add_tet_mesher: add_viewer, viewer(name), viewer_names()); a pure ABI addition (an ABI review row, as v18.0.0's were). cmd_view and cmd_screenshot ask the registry before their fallbacks. polyscope_view.cpp built as meshioplusplus_plugin_polyscope (MIT) through meshioplusplus_add_plugin, added to 4.8.1.3's archive; --version lists viewers.
    • Probe. A C++ test registers a fake viewer and view reaches it (it does not compile today); the cli-polyscope job (ci.yml lines 2391–2479) loads the plugin instead of linking it. After 4.8.1.

4.9 An MCP server for programs that embed meshio++ ​

The MCP server is a Python process. A program that links meshio++ as a C++ library, through the C API or the single header, or that loads the WebAssembly package, has the operations but no way to offer them to an agent, and the Python-free native CLI has no mcp verb. L, design pass first

  • State today. Native pieces that exist: detail/json_value.hpp (core-private: JsonValue, parse_json, write_json, non-finite numbers written as null), which the single header and WASM can carry, unlike the pipeline's JSON front end and pipeline_report_json, which need the optional nlohmann; src/cpp/cli/json_out.hpp (JsonOut, a streaming writer with no truncation and no non-finite count); the registry; apply_pipeline_step and run_pipeline_steps (include/meshioplusplus/operations/pipeline.hpp lines 191 and 198) over PipelineStep {mOp, mParams}, with the step table generated from tools/pipeline_steps.json (111 steps with each key's kind and default). The Python side: sandbox, read cache (_CACHE, _cache_limit, _cache_key; _tools.py lines 450–477), sanitizer (_MAX_ARRAY = 1000), 203 tools of which about 100 are named after a pipeline step and about 104 are not (generators, inspection, multi-mesh operations, datasets, training, rendering). No native MCP code exists.
  • 4.9.1 Design pass. Decide and write down in a doc/mcp_native_plan.md:
    • The native tool set: read/info/convert/formats/sniff through the registry; one tool per pipeline step type, dispatched through apply_pipeline_step and described from pipeline_steps.json (never a second dispatch); the generators and the two-mesh operations (merge, interpolate, diff, hausdorff), which the pipeline's v2 steps partly cover. Python-only tools (training, datasets, Arrow and Parquet export, the viewers) are recorded exemptions.
    • One source of truth for schemas: 4.4.2's generated schema file, committed, with the Python registration and the native server both checked against it; pipeline_steps.json gains the per-key descriptions it lacks.
    • The JSON layer: promote detail/json_value.hpp to what the server uses for parsing and writing, with the truncation wrapper and the non-finite count added there; the pipeline's JSON front end moves onto it too, which removes the nlohmann requirement from pipeline_report_json (a prerequisite, and a gain on its own for the single header and WASM).
    • Whether the Python server delegates its sandbox and sanitizer to the core once they exist there, or keeps a twin held equal by tests. Recommended: keep the twin for one release, then delegate.
  • 4.9.2 The core server. An installed header meshioplusplus/mcp.hpp: McpServer with HandleMessage(std::string_view json) -> std::string (JSON-RPC 2.0: initialize, tools/list, tools/call, ping), so the host owns its transport; ServeStdio() for the common case; McpOptions (sandbox root, read-cache budget, tool allow-list, host name and version); AddTool(name, schema, callback) for host tools. The sandbox, cache and reports keep the Python server's contract (path sandbox, read cache, strict JSON): realpath confinement, LRU eviction within a byte budget keyed on the file's identity, NaN as null with a count, arrays longer than 1,000 elements truncated. HTTP and the dashboard stay Python's.
  • 4.9.3 Surfaces. C: an opaque mio_mcp_server with mio_mcp_server_create(const mio_mcp_opts*), _handle(server, json, out, out_len) in the snprintf convention, _serve_stdio, _free, and mio_mcp_server_add_tool taking a callback with a user pointer: the header's first callback typedef, so state its threading and re-entrancy rules there. The native CLI: a mcp row in kCliVerbs (src/cpp/cli/main.cpp from line 10042). The installable C++ API and the single header. WASM: mcpHandle(json) and an integer handle in the module table, the convention js_bindings.cpp lines 10441–10462 explain. Thin Fortran, Julia, R and Octave wrappers over the C entry points.
  • 4.9.4 Tests and docs. A parity guard beside test_every_operation_has_a_tool: every native tool is a Python tool of the same name and schema, every Python tool is native or a recorded exemption; a scripted JSON-RPC session against the native CLI in the mcp job (ci.yml lines 1972–2031, which already runs both SDK majors); a fuzz target on HandleMessage (3.12.5); an Embedding the server section in MCP and the C, WASM and C++ pages; an ABI review row for the new header.
  • Probe. meshioplusplus mcp is an unknown verb in the native CLI, and mio_mcp_server_handle does not exist.

4.10 Interop phase 3: scikit-fem and xarray ​

Bridges in the interop pattern of 4.3. No module imports either today; verify first each package's current data model. The gmsh Python API is not one of them: it is GPL and in-process, which the decision on Gmsh's code excludes (non-goals). S each

  • 4.10.1 scikit-fem. to_skfem(mesh): one cell type, to MeshTri/MeshQuad/MeshTet/MeshHex and their second-order variants, p as points transposed and t as connectivity transposed with skfem's local ordering (the hexahedron differs from VTK's: a permutation in the payload layer, tested against skfem.io.meshio.from_meshio, which is the reference implementation of the same mapping for meshio); cell regions to subdomains, side regions to boundaries through (cell, local facet) to skfem facet indices. from_skfem. Extra skfem.
  • 4.10.2 xarray. to_xarray(mesh, location="point"): a Dataset with a point (or cell) dimension, a component dimension for vectors and tensors, the coordinates as a coordinate variable; for a sequence, sequence_to_xarray(path) adds a time dimension lazily through dask.delayed over the sequence reader, which is what makes it more than to_pandas. With 2.4.3.2, UGRID attributes on the dataset so xugrid and uxarray accept it. Extra xarray.
  • Exports, _NOT_TOOLS, gated tests, CI install line and page section as in 4.3.

4.11 A .NET binding ​

Over the C API, after Rust (4.2). Desktop pre- and post-processors and their plugin APIs are largely .NET (Rhino and Grasshopper, the Ansys ACT and SpaceClaim APIs, Femap's and Simcenter's automation). M

  • Design. bindings/dotnet/MeshioPlusPlus/: LibraryImport source-generated P/Invoke declarations over meshioplusplus for net8.0; handles as SafeHandle subclasses calling the _free functions; option structs as [StructLayout(LayoutKind.Sequential)] mirrors initialised by the *_opts_init calls, with a generated layout test against the sizes the C header documents (the check c_api.cpp's static_asserts make on the C side); getters as ReadOnlySpan<T> over the borrowed pointer, valid until the next mutation (document it; a debug build tracks a version counter to throw on stale spans); errors as MeshioException from the status and mio_last_error(). Generate the declarations from the header with ClangSharp, committed and freshness-checked, since 639 functions are too many to keep by hand.
  • Steps. The project and tests (xUnit, ported from the Julia sections as in 4.2); the parity guard of 4.2 extended; a dotnet CI job on the three platforms against the built C API; a NuGet package with runtimes/<rid>/native/ libraries from the C API build of cli.yml's toolchains; doc/dotnet.md, sidebar, AGENTS.md's lists and version table.
  • Not queued. Go, Java and Swift: no consumer has asked, and each would follow the same pattern. A native Node addon: the WebAssembly package runs in Node.

4.12 A Jupyter widget ​

  • State today. view() in a notebook embeds a self-contained page per cell: build_page (src/python/meshioplusplus/_viewer_browser.py line 170) fills three payload slots of _viewer_assets/viewer.html (a base64 VTP, stats, the view configuration) and view_browser (255) shows it as a data: URL iframe (303–310). It talks to nothing: no picked cell comes back to Python, and a changed mesh re-sends the 1.5 MB page. The viewer's state is window.__viewerState (src/viewer/src/main.ts, with picked and selection).
  • Steps. An anywidget MeshWidget (src/python/meshioplusplus/_widget.py, a widget extra): the front end is an ES module built from src/viewer by a third Vite target (build:widget, beside build:web and build:embed) exporting render({model, el}); traitlets for color_by, time_step, clip, camera (Python to JS) and picked, selection (JS to Python, wired where __viewerState is written); the mesh crosses as a binary buffer (the VTP bytes, not base64 in JSON), re-sent only when it changes. view(backend="widget") returns it; the iframe stays the default and the fallback. Tests: a Playwright test in tests/viewer/ loading the module with a fake model; a Python test of the traitlets without a front end. A notebook, viewer, _NOT_TOOLS.
  • Done when. Picking a cell in JupyterLab sets widget.picked in the kernel. S–M

4.13 A VS Code mesh preview ​

A custom read-only editor that opens a mesh file in the browser viewer through the WebAssembly package, with the inspection report beside it. It is what fuzzing already says exists (3.16.4). S–M

  • Steps. src/vscode/: a package.json contributing customEditors for the extensions the lean artifact reads (generated from the registry's extension table), an extension.ts whose CustomReadonlyEditorProvider posts the file's bytes to a webview that loads src/viewer's build:web output and the lean @meshioplusplus/wasm artifact (no native code, no Python; loadMeshioPlusPlus, src/wasm/src/index.mjs line 471), and shows readMetadata and checkConformity in a side panel. Bundled with the viewer's Vite build; a strict webview content-security policy (the readers parse untrusted files: the page gets no network and no file access beyond the posted bytes). Tests with @vscode/test-electron in a vscode CI job; packaging with vsce in the release workflow; publication to the Marketplace and Open VSX.
  • Done when. Opening bracket.vtu in VS Code shows it with its cell counts.

4.14 A C++20 named module ​

import meshioplusplus; beside the headers. M, design pass first

  • State today. 283 installed headers under src/cpp/include/meshioplusplus/ with no umbrella header; the only single entry point is the amalgamation (src/single_include/meshioplusplus/meshioplusplus.hpp, 308,816 lines). CMake's floor is 3.15, and module scanning is switched off for the tidy audit (CMAKE_CXX_SCAN_FOR_MODULES=OFF). Targets are exported from CMakeLists.txt lines 1610–1966 with components C and CXX.
  • Decisions to take first. (1) The module's shape: a module interface unit meshioplusplus.cppm with the headers in the global module fragment and export using declarations for the public names (no source change, and the ABI is the headers'), against re-declaring; recommended the first. (2) What is exported: everything outside detail::, generated from the headers by a script so it cannot drift, with a freshness check. (3) Whether the single header can be the global fragment, which would make a header-only module possible; measure its compile time first.
  • Steps. An umbrella header meshioplusplus/meshioplusplus_all.hpp (generated; useful on its own); option(MESHIOPLUSPLUS_BUILD_MODULE) guarded by CMAKE_VERSION >= 3.28 and a scanning-capable compiler, adding a FILE_SET CXX_MODULES to the installable core targets and exporting it; a second consumer project tests/consumer_module/ built in cpp-install on GCC 14, Clang 17 and MSVC; C++ API.
  • ABI. Unchanged: the module wraps the same declarations. An ABI review row for the new umbrella header.

4.15 The training-set stores in the core ​

Reopened from the non-goal that keeps pmsh, zarr, cae and usd in Python. For usd and cae its reason holds. For zarr and pmsh it now costs something: a dataset meshio++ itself wrote cannot be opened from the browser viewer, the dataset manager, Julia, R or the native CLI, and the parity decision's "a Python-only path has no twin to check it against" applies to them as to any format. M, design pass first

  • State today. zarr/_zarr.py writes the PhysicsNeMo layout with zarr-python 3 only (Zarr v3): root attributes (physicsnemo_mesh_type, __tensordict__, provenance), arrays points and cells, groups point_data, cell_data, global_data, chunks of at most 200,000 rows, Zstandard level 3 or none (lines 81–166). write_dataset's store (_ml.py near lines 617–650) writes one group per entry with whichever zarr major is installed and its default codecs (Blosc under v2). pmsh/_pmsh.py writes a tensordict memory-map tree: meta.json files and raw little-endian blobs. In the core: the JSON DOM (detail/json_value.hpp), vtk_codec_decompress_block for zlib, LZ4, Zstandard and LZMA (detail/vtu_binary.hpp lines 107–157), but Zstandard and LZ4 are off by default (CMakeLists.txt lines 264–265; on in the wheels and the full WASM artifact) and there is no Blosc.
  • Decisions to take first. (1) Scope: pmsh first (JSON and raw blobs: no codec, days of work) and Zarr v3 with the two codecs the zarr writer itself uses; Zarr v2 and Blosc stores are refused by name, with write_dataset changed to write v3 with Zstandard so that everything meshio++ writes is readable. (2) Local directories only: no object-store client in the core; the browser reads a picked directory through the existing workspace layer. (3) A build without Zstandard reads uncompressed stores and refuses compressed ones naming the option, the TecIO pattern.
  • Steps. By checklist F: formats/pmsh.{hpp,cpp} and formats/zarr.{hpp,cpp} (directory formats: the registry's directory sniffing, as OpenFOAM and Elmer); the Python packages become shims with the current code as the twin (plain_shims); writers too, by the parity decision; conformance declarations; remove the Python-only notes on the binding pages.
  • Probe. meshioplusplus info store.zarr in the native CLI, and the dataset manager opening a .pmsh entry.

5. Performance ​

Admission: a slowdown in a path a user hits, measured or verified in the code, with the shape of the fix named. Every item lands through landing a performance change: output byte-identical across backends, thread counts and the baseline digests, SEQ not slower, the ABI tier stated. The section closed in October 2026 with its decisions recorded in the non-goals; the November 2026 audit reopened it with what those decisions do not cover, and the review later that month reopened two decisions with a narrower shape (5.14, 5.15). The impact estimates come from reading the code and from the tier rows of benchmarks and the XL spike, not from new measurements: each item's first step is the measurement its probe names.

The tools every item uses. Rows are defined in src/cpp/benchmark/bench_ops.cpp through row(name, lambda) (line 521), read_row(op, format, ext, mesh) (774), file_row (801) and write_row (921); tiers are S 12, M 30, L 50, XL 120 cells a side (line 378). tools/bench_ops.sh [OUT.csv] [BACKENDS] [THREADS] -- --hash --tier S sweeps backends and threads and compares digests against BASELINE=<csv>. python3 tools/bench_instructions.py --check benchmark/instructions_S.csv is the pull-request gate (1% above the baseline fails; --record re-records it, on CI's ubuntu-24.04 toolchain, saying why). benchmark/bench.py --sizes S,M,L --formats a,b times formats from Python.

5.1 The harness first ​

The items below are only provable where the harness sees them. S–M

  • 5.1.2 The missing rows.
    • Result-file readers. lsdyna_d3plot, nastran_op2, ansys_rst and xplt have no row in bench_ops.cpp or benchmark/instructions_S.csv, though each has a registry writer, so read_row("read_d3plot", "lsdyna_d3plot", …) works as it does for the 18 readers that have rows; give each a multi-step file (the readers' cost is in the state records). benchmark/bench.py's all_formats() (lines 100–135) probably sweeps them already from Python: confirm, and record their first numbers in benchmarks.
    • Integrations with no benchmark at all. The WASM ingestion of a JS mesh (a tests/wasm/bench.mjs timing writeMesh and readMesh on a 1M-tetrahedron mesh under Node: 5.9's probe); the physics-ML Python layers (benchmark/bench_ml.py over sample_grid, select_points, neighbor_pairs and one dataset write); the ParaView and Blender converters (_paraview.py's payload functions and src/blender/addon/formats.py, timed without the host application).
    • An item below whose operation has no row adds one first, as the landing rules require: 5.14 needs transform, mirror and a pipeline row containing a transform.
  • 5.1.4 decimate_volume's ballooning star.
    • State today. The row runs at tier S only (bench_ops.cpp lines 1066–1080). In src/cpp/src/operations/decimate_volume.cpp the greedy loop (while (true) at line 401) pops a min-heap of (regime, score, lo, hi, versions) (lines 334–336, scored by dv_score, 338) and collapses onto one survivor whose star then grows; guard 6, the inversion test, walks every tet of both endpoints' stars (lines 526–540). After the ring cache of lines 389–398, benchmarks records 0.96 s and 10.3 s for sides 12 and 14 with a 696-tet star and 234,410 rejected collapses; the 110 s for 24,576 tets quoted earlier predates that cache and tier M is not re-measured.
    • Steps. (1) Re-measure sides 16 and 30 and record them. (2) Bounding the star changes which collapses happen, so decide the operation's output first: a valence cap (mMaxValence, a collapse refused when the survivor's tet count would exceed it) against a score penalty proportional to the merged star's size; implement both behind a private switch and compare on the tier S and M cubes and one graded Netgen mesh: cells removed for the target, minimum dihedral angle and scaled Jacobian of the result (compute_quality), and time. (3) Ship the better one as the default if quality is no worse than today's, else as an option; add the tier M row; re-record the instruction baseline, saying why. A new option field is a Tier A layout change to DecimateVolumeOptions.
    • Done when. The row runs at tier M in seconds and decimate_volume states the rule. M

5.2 The wheels' parallel runtimes ​

The Linux wheels build the THREADS backend since v19.0.0 (wheels.yml lines 143–147; installation), a std::thread pool in the core that needs no runtime library. S

  • 5.2.1 The measurement. One manylinux wheel from a wheels.yml dispatch against the last SEQ wheel (v18): python benchmark/bench.py --sizes L --formats vtu,xdmf and tools/bench_ops.sh at 1, 4 and 8 threads on the same machine; numbers into the table of benchmarks, whose last sentence names this probe.
  • 5.2.2 macOS. The base wheel installs Homebrew's libomp (CIBW_BEFORE_ALL_MACOS, line 112), sets no backend (so AUTO picks OpenMP) and no repair command, leaving cibuildwheel's default delocate. Add delocate-listdeps --all on the built wheel to the job's test step and assert libomp.dylib is bundled; if it is not, switch macOS to THREADS too. Then replace "to be confirmed in a wheel listing" in installation (line 53) with the result.

5.5 Operation hot spots ​

  • 5.5.2 optimize_volume's worst-cell smoothing.
    • State today. The profile at tier S puts 73% of the row in optvol_smooth_worst (src/cpp/src/operations/optimize_volume.cpp lines 967–1114). Per vertex and iteration (8): angles_at(x) computes all six dihedrals of every star tet (line 1015); the central differences are already limited to the tets in the active band (lines 1036–1056); the Frank–Wolfe step takes at most 64 iterations; then a backtracking line search of 14 halvings calls f_at (line 1001), which evaluates volume and optvol_min_dihedral (lines 438–469: six edges, two cross products and a square root each, with the acos skipped where it cannot be the least) for every star tet at every probe. The sorts of the face and edge tables are 7.4%, the passes that use them 21%.
    • Steps. What is left is the line search and angles_at. (1) Per probe, evaluate only the tets whose least angle can fall below the incumbent within the step: bound each tet's angle change by the step length over its inradius (a Lipschitz bound computed once per vertex), and skip tets whose margin exceeds it; the argument that skipped tets cannot become the least is the correctness condition and goes in the page. (2) Early exit of f_at as soon as one tet is below the acceptance threshold fx + 1e-9, which cannot change the accept/reject decision. (3) Reuse angles_at's per-tet edge vectors for the first probe.
    • Constraints. The determinism pins of optimize_volume: serial, fixed order, byte-identical across backends; (2) and (3) change no arithmetic, (1) must be shown to change no accepted step on the harness (identical digests). tools/optvol_probe.py (needs Netgen) measures the result on Netgen's meshes.
    • Done when. The optimize_volume row's instructions drop by a third with digests unchanged. M

5.8 HDF5 ​

  • 5.8.2 The library lock around library calls only.
    • State today. src/cpp/src/detail/library_lock.hpp: LibraryLock (RAII over a recursive mutex) and LibraryUnlock, which gives the lock up for a scope and retakes it; used by the parallel deflate (detail/hdf5_util.cpp lines 231 and 321) and the VTKHDF reader's assembly (formats/vtkhdf.cpp lines 1100, 1120, 1654). The writers hold the lock for their whole body: write_vtkhdf (line 1577), the series writer's methods (2048–2169), write_xdmf (formats/xdmf.cpp lines 829–831) and xdmf_time_series.cpp (eight sites). Giving it up around vtkhdf_collect (line 1231) was tried and measured no change on four concurrent writes of a 125,000-hexahedron box (0.63 s either way), the deflate being the bulk (benchmarks).
    • Steps. Reopen only on a profile that shows preparation dominating: an uncompressed write of a mesh with many small arrays, or XDMF's light-data assembly. Then split each writer into prepare (no library call, no lock) and write (locked), which 3.10.3's annotations can enforce. The invariant, no two threads inside the libraries, is unchanged.
    • Done when. Either a profile is recorded showing nothing to gain, and the item closes into the non-goals, or four concurrent uncompressed writes scale. S–M

5.9 WASM: bulk copies of JS arrays ​

  • State today. bindings/wasm/js_bindings.cpp has 47 emscripten::vecFromJSArray calls, element by element through embind, each followed by a second copy into the NDArray: float64_ndarray_from_val (line 541), int64_ndarray_from_val (548), ndarray_from_js_array (589), the polyhedron CSR (701–705), all reached from val_to_mesh (681: points 688, cells 754, data 770–813, regions 838). The other direction already copies in bulk through typed_memory_view (float64_array_from, line 241, and the arr.call<void>("set", …) sites near 336 and 2871).
  • Steps. One helper fill_from_js(val, T* dst, n): when the value is a typed array of the destination's element type, val(typed_memory_view(n, dst)).call<void>("set", v) writes straight into NDArray::Uninit storage; when it is a typed array of another type or a plain Array, keep the element path (or convertJSArrayToNumberVector, verify against emsdk 6.0.3's val.h). int64 from a BigInt64Array is the case to test: the view type must match. Replace the three helpers' bodies; the 47 sites then go through them.
  • Probe. tests/wasm/bench.mjs (5.1.2) writes a 1M-tetrahedron JS mesh with writeMesh before and after; tests/wasm/smoke.mjs unchanged; tests/python/test_wasm_parity.py unaffected. S

5.11 Sequences ​

The sequence driver keeps at most one mesh alive, by design (the streaming guarantee). M

  • State today. run_sequence_pipeline (src/cpp/src/operations/sequence.cpp line 1170) reads each step through sequence_read_step (528: sets the step and calls registry_read), in the loop at 1232–1240; the twin is _sequence.py (run_sequence_pipeline, line 1287). Document keys are parsed in pipeline.cpp lines 4259–4331 (Version, Input, Operations, Output, Mode, Parallel, Workers, Resample) and _SEQUENCE_DOC_KEYS (_sequence.py 258). The in-file-timeline readers re-read geometry at every step (read_xdmf, read_exodus, read_vtkhdf, read_lsdyna_d3plot); no series reader class exists. The PhysicsNeMo pairing reads series[step] and series[step + offset] (physicsnemo/__init__.py lines 454–455), so consecutive samples read each step twice.
  • Steps.
    • 5.11.1 Read-ahead. A Prefetch: 1 document key (default 0; add it to both key lists and tools/pipeline_steps.json's document section): step k+1 is read on a second thread while step k computes, under LibraryLock for the HDF5 and netCDF formats, with the cost of two meshes alive stated beside the streaming guarantee. Output order and bytes unchanged.
    • 5.11.2 Fields-only reads. A ReadOptions flag mFieldsOnly with a Mesh* to reuse (points and cells kept, data replaced), honoured by the four readers above when the file's geometry is static (XDMF's shared grid, Exodus always, VTKHDF's NumberOfPoints constant, d3plot without adaptivity) and ignored otherwise; the driver sets it from the second step on. ReadOptions is pinned at 72 bytes in test_abi_layout.cpp: use its reserved space if any, else this is a Tier A change.
    • 5.11.3 One read per step in the pairing. A two-entry cache in _read_sample keyed by (series, step); the same in _temporal.py (line 171).
  • Done when. A 100-step XDMF series converts with one geometry read (a counter in the test), and the pairing's read count equals the number of steps plus one.

5.12 Profile-guided optimisation of what is released ​

Link-time optimisation was measured and left opt-in because counts moved both ways (benchmarks); a profile was never tried, and it is what tells the compiler which of those ways matters. M, measure first

  • State today. No -fprofile-* flag anywhere. The release CLI is SEQ and static (cli.yml CMAKE_COMMON_ARGS, lines 56–67); the Linux wheels are THREADS. MESHIOPLUSPLUS_ENABLE_LTO is wired at CMakeLists.txt lines 155–165; -ffp-contract=off at 767–769 keeps floating point the same.
  • Steps. (1) option(MESHIOPLUSPLUS_PGO "" OFF) with values generate and use and a profile directory, applied PRIVATE to the core objects, _core and the CLI (GNU: -fprofile-generate/-fprofile-use -fprofile-correction; Clang: -fprofile-instr-generate/-fprofile-instr-use with llvm-profdata merge; MSVC: /GENPROFILE//USEPROFILE). (2) Training run: meshioplusplus_bench_ops --tier M plus benchmark/bench.py --sizes M over the text and binary formats; the workload is the harness, so it is versioned. (3) Measure four builds (plain, LTO, PGO, both) with bench_instructions.py and wall time on tiers S to L; bench_ops.sh --hash must report identical digests on all four, which is the gate. (4) If the gain is consistent (no row more than 2% slower, median faster by 5% or more), add the two-stage build to cli.yml and to wheels.yml's CIBW_BEFORE_BUILD; if not, record the table and close into the non-goals beside LTO.
  • Done when. The table is in benchmarks and the release workflows either use it or the decision is recorded. The default build is untouched either way.

5.13 Browser memory: the ceiling, then a 64-bit build ​

Several decisions rest on the browser's heap: the viewer's applies are not cached because a second copy of the mesh shares it with MEMFS, and the ceiling is a mesh of a few tens of millions of cells. S, then M, verify first

  • State today. The link options (CMakeLists.txt lines 1849–1857) set -sALLOW_MEMORY_GROWTH=1 and no MAXIMUM_MEMORY, so the heap stops at Emscripten's default ceiling (2 GB unless raised; verify against emsdk 6.0.3). Lengths cross embind as std::size_t (arr["length"].as<std::size_t>(), js_bindings.cpp line 653) and -sWASM_BIGINT is noted at line 300. The loader picks sequential or threaded by crossOriginIsolated (src/wasm/src/index.mjs lines 68–71) and the full artifact only on request (473–480).
  • Steps.
    • 5.13.1 Raise the 32-bit ceiling. -sMAXIMUM_MEMORY=4GB on the four artifacts; a Node test that allocates past 2 GB; the largest mesh the viewer opens before and after, recorded in WebAssembly. Days of work, and it may be all that is needed.
    • 5.13.2 Memory64. Verify first: emsdk 6.0.3's -sMEMORY64=1 status, embind mapping size_t to BigInt (every as<std::size_t>() and every typed-array length the typed-array channel passes must then be converted at the boundary, and index.d.ts must not change), the dependencies of build/build-wasm-deps.sh rebuilt for wasm64, Safari's support, and the slowdown of 64-bit pointers on tests/wasm/bench.mjs. If it holds: a fifth artifact (meshioplusplus_wasm_64.{mjs,wasm}) built by build/configure-wasm.sh --memory64, chosen by the loader only on request ({ memory64: true }), as full is.
    • 5.13.3 Measure the caching decision again on whichever of the two lifts the ceiling: the non-goal on caching between the viewer's applies cites the shared 32-bit heap.

5.14 An in-place transform ​

Reopened from the decision against dropping transform's copy, which stands for transform itself: its result aliases nothing, and that is its contract. An explicit second entry point is an addition. S–M, measure first

  • State today. Mesh transform(const Mesh&, const AffineTransform&, bool rotate_vector_data) (include/meshioplusplus/operations/transform.hpp line 87; src/cpp/src/operations/transform.cpp lines 203–270) copies connectivity (transform_owned_copy, line 222), point, cell and field data. The pipeline's pipe_apply_transform already takes its mesh by value (pipeline.cpp line 491), then copies it again inside transform. The uniform mesh API has Points() const and AssignPoints only (mesh_api.hpp lines 52 and 76): no mutable points accessor. No benchmark row holds a transform: pipeline_smooth_surface is the only pipeline row, and transform, mirror and flip_normals have none.
  • Steps. (1) Rows first: transform, mirror, flip_normals, and pipeline_transform_chain (three transforms then a write), at tiers S to L. (2) Measure what share of each the copies are (a build with the copies stubbed, for the number only). If under a few percent of the pipeline row, record it in the non-goal and close. (3) Otherwise: void transform_inplace(Mesh&, const AffineTransform&, bool) in the same header (a pure addition: ABI review row), implemented by taking the points array out of the mesh, mutating it when owned (MakeOwned when it is a view) and AssignPoints back, rotating vector data in place likewise; the pipeline's Transform, Mirror and FlipNormals steps call it on their by-value mesh. Surfaces: C mio_transform_inplace; Python transform(mesh, …, inplace=True) only when the mesh's arrays are owned and writable, a named refusal on a borrowed view; the flat bindings through the C entry. The data-only operations (data_drop, data_keep, data_rename, data_calc, data operations) are the same question with the points untouched: measure them in the same pass.
  • Done when. pipeline_transform_chain drops by the measured share with every other digest unchanged, or the number is in the non-goal.

5.15 Vector kernels behind runtime dispatch ​

Reopened from the decision against SIMD intrinsics and -march flags, whose reason was portability across wheels, WASM and the release binaries, with "revisit only if the harness shows a kernel dominating". The per-row profiles now exist, and function multi-versioning compiles the variants into one portable binary. M, measure first

  • State today. No intrinsics, target attributes, NEON or WASM SIMD anywhere, and no dispatch scaffolding. Candidates, all integer or byte kernels: base64 (vtub_b64_groups, b64encode, b64decode; src/cpp/src/detail/vtu_binary.cpp lines 60, 89, 224); byte swapping (bswap_copy, bswap_inplace; include/meshioplusplus/detail/byteswap.hpp lines 105 and 140); number text (parse_double, snprintf_c; detail/fast_number.hpp lines 157 and 378); the BVH's box tests (tribvh_slab, detail/tri_bvh.cpp line 88; sd_box_lb2, detail/surface_distance.cpp line 483); the gathers (cell_centroids, BoundingBoxUtils::FromCoords).
  • Steps. (1) Measure: perf record on the tier L rows read_vtu_ascii, a base64 vtu read and write, a big-endian legacy VTK read, distance_to_surface, and report each candidate's share in benchmarks. (2) A kernel qualifies only if its vector form is bit-identical to the scalar one: byte and integer kernels are; the floating-point box tests are comparisons, which are; reductions (the centroid sums) are not and stay scalar. (3) For the two largest shares: a detail/dispatch.hpp (MIO_TARGET_CLONES("avx2", "default") on GCC and Clang for x86-64; NEON is baseline on arm64 and needs no dispatch; a manual cpuid function pointer for MSVC; -msimd128 for the full WASM artifact only), with the scalar body unchanged and the vector body written with the compiler's vector extensions or Highway (Apache-2.0) if the extensions do not reach. (4) A test that runs each kernel's variants against the scalar one on random input, and the determinism check across the dispatch levels (an environment override MESHIOPLUSPLUS_SIMD=off).
  • Done when. The measured rows gain what (1) predicted with identical digests, or the measurement shows no kernel above a few percent and the non-goal stands.

6. Code duplication ​

Admission: copies the duplication measure finds, listed by place so each can be taken alone, without a feature waiting on it. An item names the files and line ranges, what is repeated, and the shape a shared version would take; it closes when the copies call one implementation or when the copy is shown to be a table that only looks repeated. Items whose shared home is one of §3.4's kernels say so and are counted there, not here. Line numbers are those of master at db32c73 (November 2026) and drift as the files change: rerun the report before starting an item.

Measured with tools/dup_report.py --lang all --json (static analysis), which works like SonarCloud's detector: C++ 22,294 of 244,439 code lines duplicated in 730 files (9.1%), Python 12,468 of 136,064 in 469 files (9.2%). SonarCloud's first analysis (§7) counts less, 4,974 duplicated lines in 314 blocks across 110 files (0.9%), because its C++ detector needs longer runs; its ranking of files agrees with the one below at the top (cli/main.cpp, the Nastran pair, mcp/_server.py, c_api.cpp, _core.cpp), and it found the copies of 8.10 the local report did not scan until it took the R binding's C sources. The report lists regions shared by two files; the copies within one file, which dominate the four worst files, were found with the same tokenizer and window over each file alone. Probe for every item: the file's duplicated lines drop in the report, tools/bench_ops.sh --hash against a BASELINE reports identical digests, and the instruction-count gate passes. A change to an installed header follows the ABI policy.

After the shared-utilities pass (November 2026: the report's table filter, the private MathUtils, GeometryUtils, BoundingBoxUtils, FacetSlotUtils, DTypeUtils and NDArrayUtils classes of shared geometry and FEM kernels, and the Python helpers it closed items into) the report, which now skips literal tables and reads the R binding's C sources, counts C++ 21,699 of 251,295 code lines duplicated (8.6%; 19,725 of 243,917 on the earlier roots, 8.1%) and Python 8,962 of 135,752 (6.6%). The numbering below is that pass's: what was 8.2 to 8.11 is now 8.1 to 8.10.

After the binding pass (November 2026: the helpers listed in shared geometry and FEM kernels, from _forward in the MCP server to mio_r_report_list in the R binding) the report counts C++ 20,554 of 250,625 code lines duplicated (8.2%) and Python 7,214 of 133,787 (5.4%). It closed 8.1.2, 8.2.3, 8.3.1, 8.6.1, 8.8.1 and 8.8.2, the server side of 8.4.1 and the C++ side of 8.4.2; it closed 8.1.3 and 8.1.4 and narrowed 8.3.2 by the decisions below, and narrowed the rest to what it found. Two of its closures raise their file's count, because the detector normalises names and literals and so matches the rows of a table against each other: registry.cpp's one row per format (263 duplicated lines before, 424 after) and cli/main.cpp's verb table (2,270 before, 2,348 after). Each lists every format or verb once where there were four and two lists; they are tables that only look repeated, the closing condition above. The decisions it took are not open work: _core.cpp's feature flags are one #if each with nothing a table would remove; the Python render keywords and the pipeline's Snapshot options are not one list spelt twice (15 keys exist only in Python, and the colours and encodings differ: tuples against hex strings); and a verb's text and --json reports are not one-to-one (column widths, conditional lines, keys only in JSON and their order against write_mesh_cli), so a report helper is per verb, as cli_sweep_report is, not one generic field list.

After the third pass (November 2026, the second half of the binding pass: the C API's result moves, lattice extents and enum checks; the native CLI's option, warning and lattice helpers and one decimate verb body; the MCP tools' _run_op; one factory for 20 Python format shims; the Nastran writers' model building; forced inlining for two of 3.4.11's shares; one spec for the pipeline step tables; the R and Octave bindings' report and getter helpers) the report counts C++ 19,411 of 249,708 code lines duplicated (7.8%) and Python 6,500 of 132,440 (4.9%). Every item of the binding pass's list closed or became a decision except the three below. The decisions, which are not open work: _core.cpp's remaining option fills each fill one struct once, with string-to-enum conversions that carry their own messages, so a generic (keyword, member, default) table would need a converter per entry to keep them; the C API's remaining inline conversions (mio_select, mio_mesh_cad_surface2, mio_fit_cad3, mio_boundary_layer) convert their struct once; of the native CLI's verbs only check-conformity prints the same fields as text and as JSON, so the per-verb report helper has nothing to share, and the four MMG verbs share their options, parameters and summary already, the rest being each verb's own fill; the MCP tools that transform an argument before the call (a path resolved, a list built) keep their bodies, since _run_op passes only call-free overrides; the primitives' checks are one shared call each, which a combined prologue would only re-wrap; the pipeline's per-step option parsing stays hand-written in each engine, with tests/python/test_pipeline_spec.py holding both to the spec's kinds and defaults; the time-series, svg/tikz and near-miss shims differ in their signatures or core arguments; the Nastran card tables and unique-id rules (per card in HDF5, global in OP2) and the readers' per-block policies (insertion against sorted order, last- against first-value-wins) differ by design; the R builders left (data_info, data_integrate, diff) build nested lists in loops. The viewer page's repeats are vtk.js's own WebGPU shader templates inside the single-file bundle, which sonar.cpd.exclusions leaves out of the duplication measure. The numbering below is this pass's: 8.7.1 became 8.1, what was left of 8.8.1 8.2, and 8.9 8.3.

After the installed-header pass (November 2026, ABI 42: the last three items, with the OpenCASCADE build no earlier pass had) the report counts C++ 18,744 of 249,629 code lines duplicated (7.5%; 19,411 of 249,784, 7.8%, before it) and Python 6,500 of 132,442 (4.9%), and this section has no open item. It closed 8.1 (the provider's pole and knot builds, node welding, and its copies of the native fit_cad's triangulation, adaptive options, re-segmentation rung, refinement verdicts and STEP chain walks, now fit_cad_native.hpp's and step_brep.hpp's; the fit_cad goldens unchanged), 8.2 (one gather body in detail/subset_gather.hpp, forced inline into build_cell_subset and clean: split, crop, partition, clean and clean_weld within the gate; a plain inline function, or an out-of-line entry beside the body in subset.cpp, cost build_cell_subset its inlining of read_int, split +10%) and 8.3 at ABI 42 (the grid walks, the link sentinels, the series writers' mode, raw array and special members, the drawing writers' options, the B-rep face counts and the distance names; see shared geometry and FEM kernels and common utilities). The decisions it took, which are not open work: the XDMF and VTKHDF writers keep their own declarations of WritePointsCells, WriteData, Flush and the rest, whose contracts and documentation differ (buffered light data against per-step HDF5 writes), and the MESHIO and NATIVE backends keep their own CellView and region and property-set accessors: every shared form measured (a CellView template over member pointers or a traits class, a common base, a CRTP base for the region accessors) moved GCC's inlining in the operations that read cells past the instruction-count gate (linearize_ragged +3.7%, gradient_surface +1.0% to +1.04%), and their point and data accessors sit over different storage (unordered_map against NamedArrays); mPadding/mPaddingRelative are two fields of five option structs with different defaults (0 or 0.1), not one struct spelt five times; the *_from_name parsers of octree_mesh and sdf each name their own aliases and messages; the format headers' read_<fmt>_metadata and <fmt>_time_values are one documented declaration per format, a table that only looks repeated. What the report still finds across the files the pass touched is outside this section: the hand-written cross products of step_brep.cpp and fit_cad.cpp (fcn_facet_area_fraction, the patch areas) are 3.4.1's. A copy the report finds from here on is listed in this section as a new item, numbered from 6.1.


7. SonarCloud findings ​

Admission: what SonarCloud's rules report beyond duplication, grouped by rule so a rule can be closed in one pass. A finding is either fixed, or shown to be a false positive and the rule narrowed for that pattern (static analysis), so the next scan does not report it again; a finding confirmed as a defect that loses data or crashes opens a Correctness debts section at the top, as how this file works says. The live list is on sonarcloud.io.

The first analysis ran on the default Sonar way profile, with no coverage: 416,477 lines; 110 code smells (maintainability A); 226 bugs (reliability E, 121 of them blocker); 46 vulnerabilities (security E). The SonarCloud pass (November 2026, ABI 41) worked every item of the list it made, from the 376 issues the scan of 60825af held, and closed them in code where the behaviour stays the same:

  • Misread patterns. NaN tests are std::isnan/math.isnan; NDArray's default heap is out of line, where the analyzer had followed its shape loop taking no dimension and reported every write past an array's first element (68 blockers); guards and locals the analyzer can follow replace its divisions and shifts by zero; the preprocessor branches that were alike are one constexpr test each. The Python floating-point equalities (88) were each read and are exact sentinels the reference must match the core on, recorded once for the rule.
  • Smells. The MESHIO data names are sorted on each call rather than memoized from a const accessor, so a const Mesh may be read from several threads (mesh data model); atomics take the default order but one idempotent mark; the recursive and the deliberately leaked mutexes say why; parsing with side effects in && is split; string lengths are bounded or std::string_view; CLI handlers that only report return nothing; NumPy and Python idioms; the nine backtracking regular expressions are rewritten without overlapping quantifiers; the viewer's controls are labelled and its splitter follows the WAI-ARIA separator pattern.
  • Bugs to verify. None was a defect that loses data. The triangle intersection's slot count, the curve evaluator's dimension, the tensor and stiffness preconditions, the null connectivity and edge-table paths are explicit; destructors guard their logging; the C API's octree entry points keep their defaults' address in a local.
  • Vulnerabilities. Real, and fixed: the TUI music player wrote its loop to a predictable name under the temporary directory (now a mkdtemp directory), make_temp_dir and the decompressed copies were not private (now mkdtemp, mode 0700), the subprocess log was world-readable (0600), the MCP server's resolve_checkpoint followed the best checkpoint a job file recorded without confining it, and log lines carried a file's control characters unescaped (log::escape). By design, and recorded once per rule: the library's top-level modules and the trainer open what their caller names; dynlib_open loads the configured plugin; seeded sampling, a SHA-1 content key, the companion's plain-HTTP loopback URL.

What the pass could not settle here, because only a scan shows it. Configuration: sonar-project.properties (sources, exclusions, twelve sonar.issue.ignore.multicriteria entries each with its reason) and .github/workflows/sonarcloud.yml.

  • 7.1 Confirm the third pass on the next master scan. The scan of 015e109 (November 2026) left 25 bugs and 7 vulnerabilities, all closed by the second pass (9b1b757), whose full re-analysis reopened four older ones and kept four code smells. The third pass replaced xdmf.cpp's tail memset by a fill over the array's own size (cpp:S3519), put providers.cpp's two cpp:S8462 markers back on the reported lines, and recorded per file the number formatters of detail/fast_number.hpp (cppsecurity:S5145) and the embedded viewer's focusable splitter and scroll region (Web:S6845). Steps: query https://sonarcloud.io/api/issues/search?componentKeys=loumalouomega_meshioplusplus&resolved=false&ps=500 (and api/measures/component for the ratings), list what remains by rule in this section, and for each either fix it or add its multicriteria entry with the reason (false positives). Done when the query returns none that is not listed here. S
  • 7.2 Coverage on new code. The coverage job uploads the coverage-reports artifact (coverage.xml, cpp-coverage-sonar.xml; ci.yml lines 1706–1715), which sonarcloud.yml finds through workflow_run (lines 67–110) and passes as sonar.python.coverage.reportPaths and sonar.coverageReportPaths. The quality gate's Coverage on New Code condition (80%) is then measured rather than failed outright. If the gate stays red, the uncovered new lines it names are the work, or the condition is set to what the suites reach; 3.11.1 puts the same number on pull requests, where it can be acted on before merging. S
  • 7.3 The viewer's built page is stale. src/viewer/src/features/shell.ts line 39 sets the splitter's aria-valuenow when it moves, but the committed src/python/meshioplusplus/_viewer_assets/viewer.html holds the attribute once, the static one: its minified setWidth predates the change. Rebuilding needs the WebAssembly package (npm run build:embed in src/viewer, then copy dist-embed/index.html over the asset, src/viewer/README.md lines 44–108). CI's only check is a size drift of at most 10% (ci.yml lines 2171–2190), which cannot see this: add to the viewer-web job, which builds the package anyway, a step that runs build:embed and fails when the result differs from the committed asset (or commits it from a scheduled job), so the embedded page cannot fall behind again. 4.6 and 4.12 need the rebuild too. S

8. Data model ​

Admission: something files hold and users ask for that Mesh has no place for, so that every format carrying it drops it, splits it or hides it in a naming convention. Each item may change NDArray or Mesh, and then it reaches every mesh backend (MESHIO, NATIVE, KRATOS), every binding and the ABI: each starts with a design pass in the manner of the long-run spikes, which decided the last two questions of this kind (NURBS as a side channel; the isogeometric layer beside Mesh), and the outcome may again be "a convention, documented and tested" rather than a new type. Each item lists the questions its pass must answer and the option recommended from what the code shows today. The order is by how many shipped formats lose data for want of each.

What a new dtype touches, for 8.1 and 8.2: enum class DType and dtype_size/dtype_numpy_str (src/cpp/include/meshioplusplus/ndarray.hpp lines 225–270); detail::DTypeUtils (src/cpp/src/detail/dtype_table.hpp, kNumDTypes = 10) and dispatch_dtype (detail/value_io.hpp line 180); dtype_from_numpy (bindings/python/np_conversions.hpp line 103, which today throws on kinds b and c, sending the call to the Python twin); mio_dtype (meshioplusplus.h line 156, values 0 to 9); Fortran's four constants (meshioplusplus.f90 near line 2281); Julia's _DTYPES (libmio.jl line 93); R's mio_r_fill_real, which widens everything to double; Octave's dtype_of_class (util.c lines 421–433); WASM's js_typed_array_ctor and dtype_of_js_array (js_bindings.cpp lines 303 and 559); every writer's dtype switch. A new enumerator appended is ABI Tier C for C++ and additive for C, but every switch over DType in the 86 format sources must handle it, which is the real cost.

8.1 Complex values ​

  • State today. No complex dtype. The readers that meet complex data already agree on one convention: <name>_real and <name>_imag in UNV (formats/unv.cpp lines 1572 and 1639), OP2 (which converts magnitude and phase to real and imaginary first, op2_complex, formats/nastran_op2.cpp line 1905) and Nastran HDF5 (formats/nastran_h5.cpp line 522). EnSight's complex variables are skipped (2.3.3.5); Ansys .rst harmonic sets are neither read nor written (2.2.12).
  • Decisions to take first. (1) Convention or dtype. Recommended: the convention, since three formats already share it: document _real/_imag in mesh data model as the form, add helpers complex_pairs(mesh) (names that form a pair), to_complex/from_complex in Python (numpy has the dtype, so a Python user gets complex128 arrays on request) and a C++ detail/complex_pairs.hpp, and have every writer that can store complex data (UNV 2414, OP2, Nastran HDF5, EnSight, VTK through two arrays) recognise a pair and write it as one complex field. (2) Which operations must treat a pair together: transform with rotate_vector_data (rotate both), interpolate (both), data_calc (expose abs and phase). (3) A real dtype only if a consumer needs complex arrays through the C API; then MIO_COMPLEX64/128 as interleaved pairs, by the list above.
  • Steps (the convention). The page section; the helpers with tests; EnSight complex read and write under it; a conformance property complex in conformance_spec.py for the formats that hold it; data_info reporting pairs as one field.
  • Done when. One complex OP2 table and one complex UNV dataset read to arrays of the same names and shapes, round-trip through each other's writers, and to_complex returns complex128. S (convention); L (dtype)

8.2 Strings and booleans ​

  • State today. field_data is numeric arrays only in the core (unordered_map<string, NDArray> in MESHIO, NamedArrays in NATIVE, native_mesh.hpp line 450); Python's Mesh.field_data accepts any dict (_mesh.py line 200), and the conversion throws on a non-numeric entry unless lenient_field_data skips it (np_conversions.hpp lines 487–575), so a string in field_data silently selects the Python twin or is dropped. Strings a format holds live in side structs (ExodusInfo::mInfoRecords, MedInfo::mMedNom). Booleans widen to uint8 (Octave maps logical to MIO_UINT8) and come back as integers.
  • Decisions to take first. (1) Strings in field_data: recommended, a documented encoding rather than a dtype: a uint8 array of UTF-8 bytes with a companion <name>:offsets int64 array for a list of strings (the ragged layout NDArray already carries for polyhedra), written by np_conversions from a Python str or list of str and decoded back, so the core carries them opaquely and every format writer that has a string slot (Exodus info and QA records, CGNS Descriptor_t, MED descriptions, VTK vtkStringArray) can use it. (2) Booleans: recommended, a meshio:bool:<array> marker in field_data naming uint8 arrays that are masks, honoured by the numpy conversion (returns bool) and by the formats with a boolean type; no dtype.
  • Steps. The encoding in detail/string_field.hpp with tests; np_conversions.hpp both ways; the C API needs nothing new (arrays of bytes) but gets mio_mesh_get_field_string as a convenience; the four formats above; drop the silent fallback (a str in field_data no longer declines the core).
  • Done when. Mesh(..., field_data={"title": "wing"}) writes through the core (MESHIOPLUSPLUS_STRICT_CORE=1) to Exodus and reads back equal. M, design pass first

8.3 Integration-point data ​

Gauss-point results are the primary output of a structural solver, and no operation knows an array is per integration point.

  • State today. Three per-format conventions, each a cell array of shape (cells, points × width) with its layout in field_data: Abaqus .fil (abaqus:layout:<name> = [points, width], point-major, NaN-padded; names with @sp<k> section points; formats/abaqus_fil.cpp lines 1060–1148); d3plot (lsdyna_d3plot:layout:<name>, formats/lsdyna_d3plot.cpp lines 1426 and 1992); GiD (gid:gauss_points:<name> and gid:gauss_coords:<name>, formats/gid.hpp lines 275 and 303, the only one that records where the points are). MED's ELGA fields are read by the Python twin as a 3-D (cells, NGA, components) array, not written, and declined by the core (med/_med.py lines 695–719 and 1104). .rst element results and Gmsh's $ElementNodeData (2.3.6.1) have the same shape. interpolate, data_average and tessellate treat the columns as components.
  • Decisions to take first. (1) Recommended: one convention replacing the three, not a third data location: field_data["meshio:ip:<name>"] = [points, width, rule_id] on a 2-D cell array, with rule_id naming a rule of 3.4.3's quadrature table (point positions and weights in the reference element), or 0 for "nodes of the cell" (element-nodal data) and -1 for "unknown rule" (the order is the file's); the readers write both their old key and the new one for one release. A third location (ip_data) would cost every backend, binding and writer for no operation the convention cannot serve. (2) Each format's rule and point order mapped to the table: Abaqus and Ansys element libraries, LS-DYNA's through-thickness points, MED's GAUSS localisations (read from the file: MED stores coordinates and weights), GiD's internal and given rules. Where a format's order is undocumented, -1. (3) The operations: ip_to_points (extrapolate with the element's shape functions, then average at nodes: the least-squares form for rules with fewer points than nodes), ip_to_cells (weighted average), and the reverse points_to_ip for writing.
  • Steps. detail/integration_points.hpp (layout read and write, rule lookup); the readers and writers of the five formats on it, with MED ELGA written and read in the core; the two operations by checklist O; data_info reporting integration-point arrays as such; conversion MED to GiD and .fil to MED as the round-trip tests.
  • Done when. A .fil stress field converts to MED ELGA and to a nodal field whose values match Abaqus's own extrapolation (.dat printout) on the truss and a C3D8 run. L, design pass first

8.4 Data on facets and edges ​

  • State today. A Region holds a name, kind, dimension, tag and entries (include/meshioplusplus/region.hpp lines 123–148; Side entries are (global cell, local facet) rows, numbered by detail/cell_faces.hpp and cell_edges.hpp) and no values; Region is 120 bytes in test_abi_layout.cpp. A value per boundary face today means a duplicated block of surface cells with cell data, which is what the VTK family and OpenFOAM's boundary fields use (the writer maps boundary-block rows to boundaryField).
  • Decisions to take first. (1) Recommended: no change to Region; side data is cell data on the lower-dimensional block, and a side region can be turned into such a block and back: sides_to_cells(mesh, region) (the facets as a new block with a meshio:side_of (cell, local facet) cell array) and its inverse, so the formats that store values on sides (CGNS FaceCenter solutions and BC data, Exodus side-set variables, UGRID location = "face" on volumes, MED face families) read into that form and write from it. This makes the convention explicit and testable instead of per format. (2) Edge data likewise on line blocks with meshio:edge_of. (3) A data member on Region only if a format needs values on sides with no cells to hang them on.
  • Steps. The two helpers by checklist O (they are extract_skin's neighbours and reuse its facet table); the readers of 2.3.1.4, 2.3.2.3 and 2.4.3.2; a section in regions.
  • Done when. An Exodus side-set variable reads as cell data on a quad block that sides_to_cells would have produced, and writes back. M, design pass first

8.5 Hierarchy ​

  • State today. Regions are flat; nesting is / in the name (regions, lines 286–298), and the KRATOS backend is the one consumer that interprets it (BuildSubModelPartsFromRegions, kratos_mesh.hpp line 610; RestoreRegions, 995), losing only names that contain . and the mDim/mTag members. VTKHDF assemblies and .vtm flatten to it on read.
  • Decisions to take first. Whether / names plus one field_data entry are enough to round-trip the trees files hold. Recommended: yes, with field_data["meshio:tree"] as a string list (8.2) of the internal nodes that own no entity (an assembly that only groups), since leaves are regions already. Check against: Abaqus parts and instances (instances repeat a part with a transform: the tree names them, the transform goes to field_data), VTKHDF Assembly, .vtm nesting, CGNS families of families, Exodus assemblies (2.3.2.4), USD prims.
  • Steps. detail/region_tree.hpp (parse, build, parent and children queries) with the KRATOS interpreter moved onto it; the five formats' writers rebuild their nesting from it; regions reports print the tree.
  • Done when. A nested .vtm and a VTKHDF file with an Assembly round-trip with the same nesting. M, design pass first; after 8.2

8.6 Structured grids that stay structured ​

  • State today. vti, vtr, vts and legacy structured VTK are expanded to explicit cells by detail::vtk_structured_cells (src/cpp/src/detail/vtk_xml_pieces.cpp line 315): eight indices per cell for what three integers describe. Their writers accept only a mesh that detail::lattice_from_mesh (detail/grid_lattice.hpp line 205) recognises as a lattice, which is why they fail conformance on the canonical mesh. CGNS structured zones are refused (2.3.1.3) and PLOT3D is not read (2.4.2). The physics-ML grids are the opposite case: GridSpec/GridArray in Python with no Mesh.
  • Decisions to take first. (1) Recommended first step: an annotation, not a block kind: field_data["meshio:structured:<block>"] = [ni, nj, nk] (point counts) set by every structured reader on the block it expands, checked by lattice_from_mesh instead of re-deriving, honoured by the structured writers (which then also accept curvilinear lattices for vts, CGNS and PLOT3D without geometric detection), and removed by any operation that changes the block's points or cells (the pipeline clears it unless the step is data-only or a rigid transform; an explicit list, tested). This closes the three conformance failures by declaring what the formats hold, and costs no ABI. (2) An implicit-connectivity block (CellView generating corners from indices) only for 10⁸-cell voxel data: every operation's hot loop reads connectivity through CellView, and the measured cost of changing it is what kept the backends' CellViews apart (§6). XL, and only with a workload.
  • Steps (annotation). The key and its invalidation rule in mesh data model; readers (vti, vtr, vts, legacy VTK, VTKHDF image data, CGNS, PLOT3D, EnSight block) set it; writers use it; conformance_spec.py's three error declarations become lattice round-trips; to_grid(mesh)/from_grid join Mesh and the Python GridArray.
  • Done when. read("a.vts") then write("b.vts") is exact, and the same mesh after smooth is refused by name. M (annotation); XL (block kind), design pass first

9. Operations ​

Admission: an operation a mesh workflow reaches for that the core's own kernels nearly give, found by reading each operation's page for what it refuses and by comparing the surface with what users of comparable libraries call. Not the algorithms the non-goals leave to backends. Each lands on every surface by checklist O; the anchors below are the kernels to build on, located in the code in November 2026.

9.1 Surface booleans through a backend ​

Reopened from the non-goals, which record why a native mesh_boolean was not queued: the spike was exact in topology and failed on rounding its output and on coplanar contact, the two things a dedicated library spends its complexity on. The same non-goal says the answer to such an algorithm is an optional backend, and that route was not measured. M, measure first

  • State today. The prototype: mesh_boolean_spike(A, B, BooleanOp) returning BooleanResult {mOk, mFailure, mMesh, mReport} with boolean:source cell data (src/cpp/src/detail/mesh_boolean.hpp line 104). The harness src/cpp/benchmark/boolean_spike.cpp (target meshioplusplus_boolean_spike) calls it directly at line 234 and prints one JSON line per run (ok, watertight, self_intersection_free, rounding_inverted, the volume identity errors, times). tools/boolean_corpus.py has synthetic, fetch, run, summary; the weekly boolean_corpus workflow runs it. Provider kinds are structs of std::functions in include/meshioplusplus/providers.hpp with named registration (add_tet_mesher, tet_mesher(name), lines 145–166); add-on wheels follow packages/python/meshioplusplus-mmg and cmake/addon_*.cmake; plugins cmake/plugins.cmake.
  • Steps.
    • 9.1.1 A backend switch in the harness. --backend spike|manifold in boolean_spike.cpp, the Manifold path compiled only when MESHIOPLUSPLUS_SPIKE_MANIFOLD finds the library; no public API yet.
    • 9.1.2 Measure. Manifold (Apache-2.0†; verify its dependencies' licences, Clipper2's Boost licence among them†; guaranteed-manifold output from manifold input, coplanar faces by symbolic perturbation, its own WebAssembly build). Run the synthetic corpus (--synthetic 78 200) and the Thingi10K pairs on the spike's own criteria: every result watertight (surface_watertight_check), free of self-intersections (find_intersections on the output), the two volume identities, and the coplanar classes that the spike could not handle (boxes sharing a face, identical spheres, a vertex on a face). Also what it does with inputs that are not manifold (it refuses: count how many Thingi10K models that excludes, and whether repair first changes the count). Record the table in the spike's page beside its own.
    • 9.1.3 If it passes: the operation. A BooleanProvider {mName, mVersion, mBoolean(const Mesh&, const Mesh&, BooleanOp) -> BooleanResult} kind with add_boolean_provider, boolean_provider(name), boolean_provider_names(); mesh_boolean(a, b, op, options) in operations/mesh_boolean.hpp, refusing by name with no provider; point and cell data carried through Manifold's per-triangle provenance (boolean:source, and a parent cell id per output triangle so cell_data maps); a meshioplusplus-manifold add-on wheel and plugin; the full WASM artifact links Manifold's own wasm build if its size is acceptable (measure). By checklist O, with a pipeline step.
    • 9.1.4 What it unblocks. CSG of the primitives (box - cylinder) and the scan cut by a fitted solid (the non-goal Booleans of a scan with fitted solids): remove both entries when it lands. OpenCASCADE's B-rep booleans stay the route for fitted solids that must remain analytic.
  • Done when. The corpus table is recorded; then either meshioplusplus boolean a.stl b.stl --op union works with the add-on installed, or the measured reason joins the spike's and the non-goal stands.

9.2 Smooth subdivision ​

  • State today. refine splits at midpoints (RefineOptions, operations/refine.hpp line 356; refine_once, refine.cpp line 292) and subdivide is polyhedral; neither moves a point. No Loop or Catmull–Clark code exists anywhere (fit_cad's patch network fits B-spline patches by other constructions). Crease and boundary classification exists: crease_edges, crease_edges_triangles and CreaseEdge {IsBoundary, IsNonManifold, IsCrease} (src/cpp/src/detail/crease_edges.hpp), already behind feature_edges, decimate and smooth.
  • Design. A new operation subdivide_surface(mesh, SubdivideSurfaceOptions {mScheme: Loop|CatmullClark, mLevels, mFeatureAngleDeg, mPreserveBoundary, mRecordParentIds}), not an option of refine: refine's contract (points of the input are points of the output, bit for bit; green closure; hierarchy arrays) does not hold when points move. Loop for triangle blocks (Warren's weights; boundary and crease edges by the cubic B-spline curve rule; corners fixed), Catmull–Clark for quads and polygons (output all quads); creases from crease_edges at the feature angle, semi-sharp not offered. Point data is subdivided with the same masks, cell data inherited by children, regions remapped through detail/region_remap with FirstChild. Topology by one pass over an edge table (detail/edge_table as refine uses), parallel_for over points for the masks, serial numbering for determinism. A numpy twin with the same arithmetic order.
  • Tests. Limit positions on a regular patch against the closed form; a cube converging to its Catmull–Clark limit volume; a creased cube staying a cube at feature angle 30; determinism across backends (thread_sweep.hpp).
  • Done when. meshioplusplus subdivide-surface coarse.obj out.obj --scheme loop --levels 2 on every surface. M

9.3 Ray and nearest-point queries ​

  • State today. Two private-or-installed kernels and no public operation. Rays and winding numbers: detail/tri_bvh.hpp (private): build_tri_bvh(const TriangleSoup&, LeafSize), tri_bvh_ray(tree, origin, dir, TMin, TMax, Skip) -> TriRayHit {mT, mTriangle, mNormal}, tri_bvh_ray_filtered, tri_bvh_segment_hits, fast_winding/fast_windings; used by thickness, boundary_layer, tetrahedralize, octree_mesh. Nearest point: the installed include/meshioplusplus/detail/surface_distance.hpp: build_triangle_soup(mesh, region), build_distance_query, query_distances, query_closest_points -> ClosestPointHit, query_surface_projections -> SurfaceProjection {mPoint, mNormal, mDistance, mTriangle, mSourceCell, mFeature, mFound}; behind distance_to_surface. Point location in volume cells is inline in interpolate() (interpolate.cpp lines 388–440: a detail::BucketTable over simplex boxes and interp_tet_contains), with no reusable locator.
  • Design. Two batch functions first, a handle second. (1) cast_rays(mesh, origins, directions, RayOptions {mFirstHitOnly, mMaxDistance, mRegion}) -> RayHits {mHit (uint8), mDistance, mPoint, mNormal, mCell (the source cell, through the soup's parent map)} over tri_bvh_ray, parallel_for over rays; and locate_points(mesh, points, LocateOptions {mTolerance}) -> {mCell, mBarycentric or mReference} for surface and volume cells, extracting interpolate's locator into detail/point_locator.{hpp,cpp} (with interpolate on it, digests unchanged) and 3.4.4's barycentric helpers. Both rebuild the tree per call. (2) Only if a caller needs many batches on one mesh: a SurfaceQuery handle owning the soup and both trees, following the existing handle conventions: C mio_surface_query_create/_free like mio_sequence (meshioplusplus.h lines 6177–6229), Julia a mutable struct with a finalizer, R an external pointer, WASM an integer handle in the module table (js_bindings.cpp lines 10441–10462), pybind a py::class_.
  • Surfaces. Checklist O; arrays in, arrays out, so the flat bindings need no new conventions for (1). MCP tools write the hits as a point-cloud file.
  • Tests. Rays against an analytic sphere and a box; grazing rays (edge and vertex hits counted once); locate_points on every volume cell type with points on faces; agreement with interpolate on a field (f(locate(p)) equals interpolate's value bit for bit).
  • Done when. cast_rays and locate_points on every surface; the handle only on request. M

9.4 Mass properties ​

  • State today. compute_stats (operations/stats.hpp line 47: StatsReport) gives mTotalArea, mSignedVolume, mUnsignedVolume and an mCentroid that is the mean of the points, not the centre of mass; measure is GD&T, not mass. data_integrate integrates a field per region with detail::cell_measure (detail/data_ops.hpp line 243). poly_measure (detail/polyhedron.hpp line 198) returns a true centroid per polyhedral cell; closed_surface_volume is private to the boolean spike.
  • Design. mass_properties(mesh, MassOptions {mDensity (a scalar or a cell array name), mRegion, mAbout: Centroid|Origin}) returning, per region and in total: mMeasure (volume, or area for a shell with mThickness), mMass, mCentre, the inertia tensor as six components xx yy zz xy yz zx (the symmetric-tensor order the data model uses), principal moments and axes (the Jacobi solver in detail/), radii of gyration. For a closed triangle surface: the divergence-theorem polynomials (Eberly's formulation; exact for a polyhedron), sharing 3.4.2's ClosedSurfaceVolume; for volume cells: tetrahedra exactly, other types by simplexification (convert_cells' tables) so curved cells are approximated as elsewhere and the page says so; an open surface is refused by name unless treated as a shell.
  • Also. State in stats that centroid is the point mean, and add centre_of_mass there from this kernel; changing what centroid means would be a silent change of a reported number, so it is not changed.
  • Tests. Box, cylinder, sphere and torus against closed forms; a translated and rotated body obeying the parallel-axis theorem; density per region.
  • Done when. meshioplusplus mass part.stl --density 7850 --json on every surface. S

9.5 Elevation beyond order 2 ​

  • State today. ccells_elevate (src/cpp/src/operations/convert_cells.cpp line 835) refuses the full-Lagrange targets quad9, hexahedron27, wedge18 and pyramid14 (lines 845–849, ccells_is_full_lagrange, 191–194) and has no entry above order 2 (ccells_elevate_table, 168; twin _ELEVATE, _convert_cells.py line 99). The pieces exist: arbitrary-order Lagrange bases and interpolation_matrix(Shape, P, nodes, targets) (src/cpp/src/formats/lagrange_common.hpp line 721; twin _lagrange.py line 454), fixed_lagrange naming the fixed types to order 10, and the node-ordering registry (detail/node_order.hpp). tessellate handles five quadratic types only.
  • Design. convert_cells(mesh, "elevate", order=q) and "reduce", order=q for any supported shape: the new nodes are the geometry interpolated at the target lattice's nodes with interpolation_matrix (order p to q), deduplicated across cells by their owning edge, face or interior (an edge table and a face table keyed by sorted corner ids, as the order-2 path does for mid-edge nodes); point data interpolated by the same matrix; fixed types up to the registry's limit, VTK Lagrange ragged cells above. Elevating a linear mesh gives straight-sided high-order cells; curving them is project_to_cad's job afterwards. After 3.4.3's first step (the header moved out of formats/), which is a rename.
  • Tests. Elevate then reduce is the identity on points and data; a polynomial field of degree q is reproduced exactly at the new nodes; node counts against the closed form per shape; VTK reads a written order-4 mesh with positive Jacobians (the pyvista gated test).
  • Done when. convert_cells(elevate, order=3) on a tetrahedral mesh gives tetra20 on every surface, and order=2, full=True gives hexahedron27. M

9.6 Repeated and mixed green refinement ​

  • State today. refine names two limits. A later selective pass refines the green (transitional) cells directly, so repeated passes over one region degrade quality without bound. And mixed-type green (a quadrilateral closed into triangles, a hexahedron into pyramids) is not offered, because one input block would emit several output blocks, which the FirstChild cell-map contract cannot express (RefineResult::mCellMaps, operations/refine.hpp line 427; CellMapKind {Direct, FirstChild, Global}, include/meshioplusplus/detail/region_remap.hpp line 100). undo_green(coarse, fine) exists (operations/undo_green.hpp line 134) but needs the coarse mesh and undoes a single-pass hierarchy only; the hierarchy reader is detail/refine_hierarchy.hpp.
  • Steps.
    • 9.6.1 Undo before refining. A RefineOptions flag mUndoGreenFirst (from the reserved space: mio_refine_opts has reserved[5] free; the C++ struct is pinned at 120 bytes, so a new member is Tier A): before a selective pass, cells whose refine:entity marks them green are merged back to their parent using refine:parent_id and refine:cell_id alone (no coarse mesh: rebuild the parent's corners from the children's, which the templates of detail/refine_templates.cpp make possible), the parent is marked for red refinement if any child was selected, and closure runs again. Requires record_hierarchy, and raises by name without it. Extends undo_green to multi-level hierarchies as a by-product. M
    • 9.6.2 Mixed-type green. Decide the map shape first: CellMapKind::Global (one global child-range per input cell) exists internally; expose it for refine when mClosure = MixedGreen, with every binding's map accessor returning the global form. Then the templates: quad with one or three marked edges into triangles, hexahedron into pyramids and tetrahedra around a refined face. M–L, design pass first
  • Done when. Ten selective passes over the same region keep the minimum scaled Jacobian above a stated bound (a test that fails today), driven by error_size in a notebook.

9.7 Partitioning ​

  • State today. PartitionMethod {SFC, KaHIP, Auto} (operations/partition.hpp line 85); KaHIP is a compile-time option (MESHIOPLUSPLUS_WITH_KAHIP, CMakeLists.txt line 319), not a provider, called in partition_kahip_parts (partition.cpp lines 197–267) on the facet dual graph of detail::build_facet_dual_graph (detail/dual_graph.hpp line 66) with weights from mWeightsKey; without it the method throws by name (lines 274–284). METIS and Scotch are graph files only (write_graph, operations/adjacency.hpp line 133). The C API passes the method as a string (mio_partition, meshioplusplus.h line 3082), so a new method needs no C change.
  • Steps.
    • 9.7.1 A partitioner provider kind. GraphPartitionerProvider {mName, mVersion, mPartition(const DualGraphCsr&, nparts, imbalance, seed, weights) -> std::vector<int64_t>} with add_graph_partitioner, graph_partitioner(name), graph_partitioner_names(); partition's method becomes a name looked up there after sfc; KaHIP moves onto it as a compiled-in provider with its behaviour and digests unchanged. PartitionMethod gains no enumerator: add PartitionOptions::mMethodName (Tier A) or keep the enum for the built-ins and add the name beside it.
    • 9.7.2 METIS and Scotch providers. METIS (METIS_PartGraphKway, Apache-2.0 since 5.1.1†) and Scotch (SCOTCH_graphPart, CeCILL-C†, LGPL-compatible) as optional compiled-in providers and plugins, in the pattern of the Netgen provider (src/cpp/providers/netgen/), each with a CI job like kahip (ci.yml line 591). Seeds fixed, so output is reproducible per library version; state that it is not across versions.
    • 9.7.3 Connected parts. KaHIP's --connected_blocks is not reachable through the kaffpa C interface (partition records it). A method-independent post-pass mConnectedParts: label components per part with cell_components (detail/cell_components.hpp lines 55–60), keep each part's largest, and reassign every other component to the neighbouring part it shares most facets with, repeating until none remains; report the balance after it.
  • Done when. partition(mesh, 8, method="metis") works with the provider present and refuses by name without, and connected_parts=True gives one component per part on the space-filling curve's output. S–M

9.8 The isogeometric layer's open list ​

IGA marks four things as not yet. M

  • State today. iga_evaluate(info, patch, params, Derivatives) accepts 0 or 1 (operations/iga.hpp line 154); second derivatives exist internally (iga_basis_ders2, iga_eval_surface_d2, detail/iga_kernels.hpp lines 514 and 574; BrepSurfaceEval::Eval2); closest-point projection exists for B-rep faces (SurfaceProjector::Project and Polish, detail/surface_project.hpp, used by project_to_cad); knot insertion and degree elevation are public (iga_insert_knots, iga_elevate_degree); VTK Bézier cell types are in cell_type.hpp (lines 126–132) with RationalWeights/HigherOrderDegrees read by vtu_read.cpp.
  • Steps.
    • 9.8.1 Second derivatives. Derivatives = 2 in iga_evaluate over the internal kernels; IgaEvaluation::mDerivatives gains the second-order block (its layout documented); the twin repeats the arithmetic order. S
    • 9.8.2 Bézier extraction. iga_bezier_extract(info, patch): knot insertion to full multiplicity per span (the existing line-wise kernel), returning either the extraction operators per element (what an IGA code assembles with) or a mesh of VTK Bézier cells with RationalWeights, which the VTU writer already writes. S–M
    • 9.8.3 Knot removal. iga_remove_knots(patch, dir, knots, tolerance) (The NURBS Book A5.8), reporting the knots it could not remove within tolerance. S–M
    • 9.8.4 Closest point on a patch. iga_closest_point(info, patch, points) -> {params, points, distances} generalising SurfaceProjector from B-rep faces to NurbsInfo patches (curves and surfaces; volumes by inversion, later). S
  • Done when. The Not yet line of the page holds only refinement of one patch of an MFEM model.

9.9 Point-cloud conditioning ​

  • State today. estimate_normals (operations/point_normals.hpp line 112) and reconstruct_surface exist; the neighbour kernels are PointKnn (src/cpp/src/detail/point_knn.hpp line 51: exact Nearest and KNearest on a bucket grid) and neighbor_pairs (operations/neighbors.hpp line 84). Downsampling exists in Python only and by count: select_points/subsample_points with methods farthest, grid (voxel representatives then farthest-point) and random (src/python/meshioplusplus/_point_budget.py lines 95, 291, 361, 485; point budgets), over the core's farthest_point_order (detail/farthest_points.hpp line 54), which is bound to Python alone. No outlier removal and no Poisson-disk sampling exist.
  • Design. One core operation filter_points(mesh, PointFilterOptions) on vertex blocks (and on the points of any mesh with mPointsOnly), with steps applied in order: statistical outlier removal (mean distance to mK neighbours beyond mStdRatio standard deviations: PointKnn), radius outlier removal (fewer than mMinNeighbors within mRadius), voxel-grid downsampling by size (mVoxelSize: centroid or nearest-to-centroid representative; the size-driven mode subsample_points lacks), Poisson-disk sampling (mMinDistance, by dart throwing on the bucket grid in a fixed order for determinism). Returns the mesh with a point map and counts per step; point data gathered (detail/subset_gather.hpp). subsample_points' farthest and grid move to the core through the same operation so the flat bindings get them, with the Python functions as shims and their outputs unchanged.
  • Licence note. The non-goal on AGPL scan tools applies: implement from the literature (Rusu et al. 2008 for the statistical filter; Bridson 2007 for Poisson disk), not from any scan tool's source.
  • Done when. meshioplusplus filter-points scan.ply out.ply --outliers statistical --voxel 0.5 on every surface, and reconstruct_surface on its output in a notebook. S–M

9.10 Units and coordinate systems ​

9.10.1 Units of fields ​

  • State today. Length only: field_data["meshio:length_unit"] with mesh_length_unit and rescale_mesh_length_unit (include/meshioplusplus/detail/length_unit.hpp) and Python's _units.py (length_unit, set_length_unit, unit_name); used by CGNS (DimensionalUnits_t on the base, cgns.cpp lines 647–690), DXF, UNV, MED, Radioss, STEP. MED's per-field units are kept in a side channel (MedInfo::mFieldUnits, formats/med.hpp line 162; field_data["med:field_units"] in Python). A field's unit travels nowhere else.
  • Decisions to take first. (1) Representation: recommended, the seven SI exponents plus a scale: field_data["meshio:unit:<array>"] = [scale, m, kg, s, K, A, mol, cd] (8 doubles), so a conversion is arithmetic with no unit database in the core; names ("MPa") are parsed and printed in Python and the CLIs from a small table. (2) Which formats carry them: MED (per field), CGNS (DimensionalUnits_t/DimensionalExponents_t per array), Exodus (none: skip), EnSight (# units lines in the case file), UNV (the 164 dataset's unit system: derive per data type), Selafin (variable-name units, 2.4.3.4), Nastran and Abaqus decks (none: a user-declared system). (3) convert_units(mesh, system): scales points, the length unit and every array with a known dimension to a target system (SI, mm-t-s, mm-kg-ms, …), refusing arrays with no declared unit by name unless told to leave them.
  • Done when. A MED file in millimetres and megapascals converts to SI and back to the same bytes. M, design pass first

9.10.2 Georeferencing ​

Only if 2.4.3 or 2.4.6 lands, since nothing read today is georeferenced. field_data["meshio:crs"] as a WKT2 or EPSG:<code> string (8.2's encoding), set by the UGRID, LAS and ADCIRC readers and written back; reprojection as a mode of convert_coordinates (CoordinateOptions, operations/coordinates.hpp line 119) through PROJ, an optional dependency in the TecIO pattern (2.4.6.2) with a Python twin through pyproj; without it the operation refuses by name. glTF and 3D Tiles get the ECEF convention only on request. S–M


Non-goals and decisions taken ​

Recorded so they are not re-proposed as gaps. A decision is reopened when what it rested on has changed, and then it is narrowed here, with the date and the number of the item that took it up; the November 2026 review read each against what has shipped since. It reopened five (surface booleans through a backend, 9.1; decomposed files read without MPI, 2.3.8; the zarr and pmsh stores in the core, 4.15; an in-place transform, 5.14; vector kernels behind runtime dispatch, 5.15) and four formats of Considered, not queued (2.4.5 to 2.4.8), and left the rest as they stand: the reasons recorded for the native constrained tetrahedralisation, NURBS as an entity kind in Mesh, Kokkos device execution, link-time optimisation by default (though a profile was never tried: 5.12), solver-coupled learning, contact mechanics, the copyleft codes and the CAD modeller have not moved.

  • Every registered format reads and writes, in both engines — decided in October 2026, when the parity tier closed; it reversed the earlier non-goals that kept result files and the export-only formats read-only. A format reads and writes in the C++ core and in its Python twin, so it reaches every surface (C, Fortran, Julia, R, WASM, both CLIs and the MCP server's format capabilities); a core-only path is unreadable from the surfaces that have no fallback, a Python-only one has no twin to check it against. read(write(m)) equals m within the limits the format itself imposes (single precision, a cell type it has no slot for, a concept it cannot express): a loss that is a fact of the format is stated on its page and in the conformance notes, and one that is only an unwritten feature is a gap that reopens a Reader and writer parity section here. A new format or direction lands as AGENTS.md says (native writer registered in registry.cpp, parameters through WriteOptions, the Python twin behind core_declined, the arrays a reader returns written back under the same names, a SPEC declaration and a Writing section). The exemptions are recorded in tests/python/conformance_spec.py (READ_ONLY, ENGINE_EXEMPT, PARTIAL_ENGINE_EXEMPT) and checked by test_conformance.py:

    • Pure 2-D drawings. svg and tikz draw a mesh rather than store it: their readers parse only the structure meshio++'s own writers emit, return the geometry as planar (z = 0) and warn once per read that depth, and whatever else the drawing could not hold, is lost; anything else in the file is a ReadError naming the construct, never a guess.
    • ansys_rst_cyclic, which is not a file but the full rotor a cyclic .rst expands to; its write side is ansys_rst's cyclic option. step and iges outside a build with OpenCASCADE: reading a CAD model means a CAD kernel, which the meshioplusplus-occt add-on wheel (since v18.0.0) and the release binaries carry and a source build finds when it can (skin to CAD, STEP); on WASM the lean build is the exempt one, and the full build carries the kernel.
    • One engine only, by decision: GiD (gidpost and its Gauss points are implemented once, in the core), the OpenFOAM case writer and its zone, field and multi-region reading, CGNS's face-based polyhedra (the writer's winding repair is a discrete branch two implementations could resolve differently), the CAD readers, and the step writer's merged and fitted faces (faces="planar"/"fitted", with segment's primitive method that feeds them: loop walking, vertex snapping and the primitive growth are discrete branches too, so the twin writes faces="facet" byte for byte and nothing else); the Python entry points raise naming the missing core rather than guess.
  • What the parity tier left as facts of the format, each stated on the format's page and in the conformance notes, none an unwritten feature:

    • nastran_op2 writes the MSC dialect only (32-bit little-endian words, SORT1 tables). 64-bit words, big-endian files, NX/Simcenter's interlaced table names and double-precision GRIDs are read and detected, not written: a post-processor reads the MSC dialect with no flags, and whether NX accepts it is a vendor check (2.2.14). The random PSD, CRM and ATO tables are written as SORT1 magnitudes, as the reader reads them (MSC writes them SORT2, and pyNastran reads only the RMS and NO ones in SORT1); no SORT2 pivot is written. Elements on scalar points (CELAS3/CELAS4, CDAMP3/CDAMP4, CMASS3/CMASS4) and CBUSH1D and CBEND have no cell type — Mesh has no scalar-point entity to hang them on — and are skipped with a warning.
    • nastran_h5 writes a table only where an MSC sample fixes its layout: CBUSH, CGAP, CMASS and CONM1 elements, and MAT5, MAT8, MAT9, PBARL, PBEAML and PCOMPG values, have no table in the six MSC Nastran 2020 files at hand, so the elements are left out and the values go on the plain cards, each with one warning; the OP2 file holds them.
    • lsdyna_d3plot leaves the rigid roads' full rigid-body velocities (NDIM = 9) out of the state, and writes an airbag variable under a title-case name, since the names of a real file are in no manual available.
    • radioss_anim writes the nodal normals as zeros: anim_to_vtk ignores them and their 16-bit encoding is undocumented.
    • abaqus_fil writes 8-byte words only, the reader's width; a single-precision build's 4-byte layout has no description or sample.
    • ansys_rst has no complex (harmonic) results, read or written (8.1 gives them a form once a sample exists): no public sample or open reader covers them, and DPF alone reads them. The nodal coordinate systems are written as ansys:node_angles and agree with pymapdl-reader only for single-axis angles, since it applies the three turns in the opposite order.
    • xplt writes a per-surface-node variable as FMT_NODE in surface order, where the reader averages the values a node on two surfaces has into one point value, which both surfaces then write.
    • marc and marc_t19: edge and face sets stay (cell, number) field data and side regions are dropped, since mapping them to facets needs Volume A's numbering (2.2.11); the Herrmann, bubble and generalized-plane-strain types come back as the default type of their shape, and a deck holds no data arrays.
    • szplt does not write polygon and polyhedral zones: TecIO 2018.3, the newest source available, refuses them in an SZL file, so the reader refuses them by name too and the face-based zones stay with the .plt and ASCII readers. Partitioned output is for tetrahedron and brick zones of a static file only (TecIO's own limit), and a series is written whole.
    • vtx: DOLFINx and ParaView read one cell type per file, so a file of several cell types or parts is meshio++'s own extension of the layout.
  • Generators the primitives leave out — recorded when roadmap §3.1 closed (October 2026), with the parametric primitives: generators driven by a user callback (no callback crosses the flat ABI; numpy users build Mesh(points, cells) directly, and isosurface on a grid covers implicit shapes); scanned or modelled assets (the bunny, the teapot, the bracket fixture — data rather than parameters, each under its own licence); and CSG combinations, under the volume-meshing non-goal below (surface booleans were reopened as a spike and kept out on its measurements, findings) — two-body interface and contact fixtures are primitives placed with transform and joined with merge.

  • MPI in the library — none planned (C++ API); partition's ghost layers produce the halo an MPI assembly in the owning application needs. ParMmg, MMG's MPI layer, is excluded with it. Narrowed in November 2026: reading the files a parallel run left, one per rank, into one mesh needs no MPI, as the OpenFOAM and pvtu readers show, and is queued for the formats that lack it as 2.3.8.

  • Solver-coupled physics-ML — assembled solver residuals, adjoints and Sobolev training, co-simulation, active-learning labeling, adaptive remeshing driven by a surrogate, and MPI model-part gathering. Every one needs a live solver (an assembly routine, its tangent, its communicator) and meshio++ has no notion of a discrete system; they belong in the application that owns the solver, the division Symbolic and physics describes.

  • The Python-only layers stay Python — pmsh, zarr, cae and usd, and the physics-ML surface (grids, point budgets, proximity graphs, datasets, training) are export targets and tooling for a training pipeline, registered in Python rather than the shared C++ registry. A core kernel they call (the neighbour search) does not change that; tessellate moved to the core (tessellation) because the native CLI and the flat bindings needed it, and its Tessellation helpers stay Python. Narrowed in November 2026 for the two stores a dataset is written to: reading zarr and pmsh in the core, so the browser, the flat bindings and the native CLI open what Python wrote, is queued as 4.15 behind a design pass; usd, cae and the physics-ML surface stay as decided.

  • Polyscope in the release CLI executables — excluded deliberately, so view/screenshot there report the build flag rather than opening a window (viewer): the executable stays free of OpenGL, GLFW and X11. Narrowed in November 2026, once v18.0.0's plugins made it possible: a Polyscope plugin loaded beside the executable is queued as 4.8.2.

  • General volume meshing algorithms as native operations — constrained tetrahedralization of a domain, hex-dominant meshing and general hex meshing from a CAD-like boundary, boolean/CSG, boundary-layer inflation and quadrangulation are each a library in their own right. (Narrowed in November 2026: geodesic distance, once listed here, is not one, since the heat method needs only the P1 operators and the sparse solver the core has, and shipped as geodesic_distance; surface booleans were measured by a spike in November 2026 (exact in topology, but rounding its output to doubles left most curved results self-intersecting and coplanar contact unhandled, so no native operation is queued: findings, though the backend route this non-goal names was not measured for them and is, since the same month, 9.1); and v18.0.0's provider registry is the optional-backend route below, which external meshers and field-aligned quads now take as installed programs: tetrahedralize's TetGen and Gmsh and remesh_quads's QuadriFlow and Instant Meshes.) The answer is an optional backend (the KaHIP pattern), not a native implementation: volume domain meshing reaches meshio++ through tetrahedralize (v17.23.0: natively by its octree backend, the skin fitted by isosurface stuffing, and exactly by Netgen, the skin kept) and adaptation through MMG (MMG backend); boundary-layer inflation reopened through a design pass for layers grown from a surface (boundary_layer, v17.23.0), the volume behind them filled by tetrahedralize; and quadrangulation reopened for CAD surfaces only (recombination of mesh_cad_surface's own triangles by a matching, v17.23.0), field-aligned remeshing through installed programs (remesh_quads). A native constrained tetrahedralisation stays out: orient3d and insphere exist and the Delaunay kernel is short (Bowyer 1981, Watson 1981), but boundary recovery with Steiner points is where TetGen and the exact constructions spend their complexity (George, Hecht and Saltel 1991; Si and Gärtner 2005; Shewchuk 2008; Si 2015; Diazzi et al. 2023). Reopen it when a design pass shows a native recovery on the core's predicates within a stated size, measured against tetrahedralize's probes and its Thingi10K sample. Two exceptions are in scope. Surface triangulation of a CAD model is native since v17.13.0 (mesh_cad_surface): the robust predicates it needs are Shewchuk's, public domain and a few hundred lines, and a planar constrained Delaunay triangulation per face is small enough to own; this reverses the earlier decision that dropped the queued Delaunay item, for surfaces and the plane only. Lattice generation is the other: voxelize, octree, remesh_volume's isosurface stuffing and the octree volume mesher octree_mesh (stuffed tetrahedra and the all-hexahedral octree dual, uniform or graded, plan) are index arithmetic over a signed distance on a lattice, followed by projection and untangling, with no predicate to make robust.

  • A declared tolerance deciding degeneracy in mesh_cad_surface — model_tolerances widens where uses start, how far corners and uses may lie apart and which trims pair, but whether an edge is degenerate (a pole) stays on the global tolerance: an edge shorter than a generous declared tolerance is real geometry, and collapsing it would change the model rather than accept its gaps (item 21).

  • What mesh generation leaves, by decision — recorded when roadmap §6 closed (October 2026); mesh generation and its measured-and-parked notes hold the numbers:

    • No small-angle bound stated on a curved face. Corner lopping's bound (Pav and Walkington 2005) is the plane's; on a curved face lop_corners reports the angles it reaches and termination stays max_points and the insertion-radius declines. Stating one would need the paper's argument redone in the surface's metric.
    • A pinched boundary stays declined by the flattening of 6.4.4: splitting the pinch into one vertex per wedge makes the coarse mesh manifold, but the face's loops pair the pinch's edges otherwise than the wedges do, so each loop would have to be re-stitched through both copies.
    • Field-aligned quads stay with backends. Recombination of the mesher's own triangles leaves its worst quads at poles (0.12 on the sphere at h = 0.1); field-aligned remeshing is Instant Meshes (BSD-3), QuadriFlow (MIT) or QuadWild (GPL, excluded), each a library of its own. Since v18.0.0 that pattern is the provider registry, and remesh_quads runs an installed QuadriFlow or Instant Meshes as a separate process (0.25 to 0.58 on the same sphere, by seed); a linked add-on wheel extracting their solvers stays unqueued until a user needs them in the wheels or the browser. The core's cross field and quad layout (detail/cross_field, detail/quad_layout) stay fit_cad's.
    • Curved boundary layers: linearize first. boundary_layer takes linear triangles and quads; a triangle6 or quad9 surface is linearized first. Growing curved layers needs a Bernstein bound on a prism's and a hexahedron's Jacobian (the tensor bound detail/curved_quad has for quads, raised a dimension) to keep "no cell is written folded" true, and its mid-edge nodes' directions; no model has asked.
    • Mesh-generation corpora with non-commercial terms stay out. The Fusion 360 Gallery (Willis et al. 2021) permits use for non-commercial research only and asks its users to indemnify Autodesk, which an MIT project's published measurements cannot accept.
    • No numpy twin of mesh_cad_surface, triangulate_polygon_2d or tetrahedralize: a second implementation of the predicates and the triangulation could not stay byte-identical, the precedent of remesh_volume.
    • Healing the model. Sewing gaps, removing sliver faces or rebuilding pcurves changes the CAD model, which is the CAD kernel's work; mesh_cad_surface meshes around defects without writing them back. Narrowed in November 2026: the CAD kernel is reachable through the provider registry, so healing at read time, before the B-rep reaches the mesher, is the STEP and IGES readers' heal option (STEP reading); mesh_cad_surface itself still never heals.
    • Code from Triangle, TetGen, Gmsh, CGAL's GPL packages or Blossom V, under any route other than a separate process: their licences (THIRD_PARTY_NOTICES). Geogram's constrained tetrahedralisation is in that list too: it creates TetGen's AGPL Delaunay kernel. The separate-process route is tetrahedralize(backend="tetgen") and backend="gmsh" (external meshers: installed executables behind named tet-mesher providers, never linked), and the rule keeps the gmsh Python API, which runs in-process, out of the interop bridges (4.10).
    • A native parallel Delaunay kernel in the style of HXT (Marot, Pellerin and Remacle 2019): it is the fast path of a 3-D mesher the core does not have, and the 2-D triangulation of one face is small enough that faces in parallel already use every thread on a real model.
    • Advancing-front surface meshing as a second native mesher (Lo 1985, Peraire et al. 1987, Löhner and Parikh 1988, Schöberl 1997): smoother gradation, but its robustness rests on the front's intersection tests that the Delaunay route avoids, and two native meshers would double what must stay byte-identical. Frontal point placement inside the Delaunay mesher (Rebay 1993, Tristano, Owen and Canann 1998) is the alternative if the varying metric's circumcentres leave triangles poorly aligned on strongly stretched faces.
  • Contact mechanics itself — penalty or mortar assembly, friction and contact search in time need a live solver. meshio++ detects and exports the interface of one snapshot (interfaces and contact); the contact algorithm belongs in the application that owns the solver.

  • MMG in the base wheel and in the native CLI executables — it is LGPL-3.0 (GPL-3.0 with the ELAS that move needs) and an external install, so the native backend is linked dynamically into builds that opt in, and its operations throw by name where it is absent (MMG backend). v18.0.0 narrowed this for Python: the meshioplusplus-mmg add-on wheel ships it, built without ELAS, beside the MIT base wheel (installation); the same split for the native CLI, a plugin beside the executable, is queued as 4.8.1. Its WebAssembly half was lifted for the full artifact when roadmap 3.1.8 closed (October 2026): MMG is linked there statically, without ELAS, under the LGPL-3.0 relinking terms the artifact's notices state (WebAssembly); the lean artifact keeps none.

  • What the full WebAssembly build leaves, by decision — recorded when roadmap §5 closed (October 2026); fit CAD's measured and parked notes hold the numbers and the reopening conditions:

    • Constraints beyond the coaxial, right-angle and equal-radius ones. Parallel but offset axes are different surfaces, and a bolt circle or a plane at another angle needs the pattern known to the pass, so constrain_primitives stays exact only where it is exact.
    • G1 at valence 5 and 6 beyond the sweeps' reach. The refine-before-discard step of the patch network brings the largest G1 angle down on the Catmull-Clark fixtures (limitations); a construction that holds G1 exactly there (Loop and Schaefer's full scheme, or G-splines) is not implemented.
    • Genus-one-and-more homology cuts. The layout's pieces are triangle groups, so a cut along both sides of a loop has no representation; that is a change to the layout's partition, not a bounded experiment.
    • THB stays experimental, behind its knot-line fallback: a rung whose hierarchical fit misses its target falls back to knot lines, so "thb" never leaves as facets a patch "knots" fits.
  • KaHIP in the lean WASM build, ADIOS2 and TecIO in either WASM build — KaHIP ships only in the full artifact (serial kaffpa, built by build-wasm-deps.sh --full; a graph partitioner would bloat every consumer's bundle, so the lean pair keeps the SFC method with "kahip" throwing by name as the contract); no one has made a wasm build of ADIOS2 (vtx); TecIO's redistribution terms keep szplt out. Each reports by name in the lean and the full artifact alike (what stays out).

  • zstd/lz4 codecs in the lean WASM build — the lean artifact keeps zlib as its only block codec; the full one (v17.2.0) links zstd, lz4 and bzip2. A zstd-compressed VTK XML file is a meshio++ extension VTK itself has no compressor for, so vtk.js does not read it; the second reader of a zstd file is meshio++'s native and Python ones. Memory mapping and the Kokkos backend under Emscripten — CMake refuses the Kokkos backend under Emscripten, and there is nothing to map inside MEMFS.

  • Single-file output for DOLFIN, TetGen and EnSight from WASM, or lifting DOLFIN's simplicial restriction — both are facts of the formats; writeMesh's written-paths return value (v11.2.0) is the fix for the bookkeeping they cause.

  • NURBS patches and trims as a second entity kind inside Mesh — decided with the isogeometric layer (v16.43.0, findings): a model (NurbsInfo, BrepInfo) stays a side channel beside the mesh, with its own evaluate, refine and sample operations, because a first-class entity kind needs patches, control nets, weights, knots and trimming curves in Mesh, every mesh backend (MESHIO, NATIVE, KRATOS), every binding and the C++ ABI: a major version for a gain the three operations already give. Reopen it when an operation must act on patches and trims as mesh entities (mixed IGA and FEM assembly in one model, for instance, or a patch-aware partition), and start from the spike's list of what the side channel cannot do: no Mesh operation sees a patch, the model does not travel through the pipeline, and an MFEM model refines every direction alike.

  • Polyhedron blocks as a WASM gap — vtu, ensight, cgns, med and openfoam all write them from WASM; a consumer that cannot is constrained by its own data model.

  • Performance work measured or decided against — recorded when the roadmap's performance section closed (October 2026), and not re-proposed when it reopened as §5 in November 2026; a new performance change passes the gates of landing a performance change:

    • The serial phases kept for determinism: the keep-first weld loop (chains make it order-dependent), the union sweeps of split and compute_normals (cheap, and their partitions are order-independent anyway), and remesh's energy sweep (every accepted move changes what the next test reads; it has no numpy twin because a near-tie decided differently yields a different clustering).
    • A facet table cached across pipeline steps — measured (benchmarks): in a Smooth then ExtractSurface chain a cache saves at most one table build, about 3.6% of the chain on SEQ at tier L with the default ten iterations, at most 10% on four OpenMP threads and about 20% with one iteration. smooth and extract_surface do not share a record structure, so it would also need a refactor, and a cache on the Mesh is a Tier A layout change. Reopen it for a workload that chains several facet readers after cheap topology-preserving steps; the pipeline_smooth_surface row measures it.
    • Link-time optimisation by default — measured (benchmarks), November 2026: identical output and binaries a third smaller, but the instruction counts move both ways (read_vtk_ascii +40%, gradient +14%, decimate −6%), so it is the opt-in MESHIOPLUSPLUS_ENABLE_LTO rather than the default.
    • Explicit SIMD intrinsics or -march flags — portability across wheels, WASM and the release binaries is worth more than the scalar kernels cost; revisit only if the harness shows a kernel dominating. Narrowed in November 2026: the decision stands for flags and for intrinsics compiled unconditionally; variants selected at run time inside one portable binary, for kernels whose vector form is bit-identical, are to be measured as 5.15.
    • Kokkos device execution — every parallel_for body captures host pointers (parallel.hpp); the GPU route is the DLPack/CuPy handoff (GPU handoff).
    • An occupancy pyramid over the distance query's bucket grid (formerly roadmap 5.3.1) — measured in November 2026 and not taken: descending the grid's occupancy at coarser block sizes, best first with Euclidean bounds, was 2 to 3 times faster than the Chebyshev-shell search at tier L, but a bounding-volume hierarchy of the triangles (formerly 5.3.2, which had been decided against because its visit order was thought to make the equidistant-triangle tiebreak observable, a reason the total order on (distance², triangle id) answers) was a further 2 to 4 times faster with its build included, on uniform and graded targets alike, and replaced the grid in ABI 40 (benchmarks). A cell's bound is the cell's, so a point far from the surface still opens every bucket of a patch around its nearest one; a box is as tight as its triangles.
    • Tuning the pure-Python fallback readers — the fix is making the C++ path accept the file, not a faster fallback; _decimate.py's heap-based twin is deleted once decimate accepts its inputs, not optimised.
    • Dropping the copy in transform — transform copies the connectivity and data it does not change (through transform_owned_copy, as 17 operation files copy through clone_mesh, clone_geometry or quality_clone_mesh), and the Python twin copies too; that copy is what makes the result a new mesh that aliases nothing, so removing it is an API change, not an optimisation. Narrowed in November 2026: that holds for transform; a second, explicitly in-place entry point changes no existing contract and is to be measured as 5.14.
    • Caching between the browser viewer's applies — every apply re-reads the staged file and replays the whole pipeline, which is what makes undo exact. Measured with the typed-array channel (benchmarks), the read is at most 2 to 25% of an apply that smooths and up to 69% of a quality-only apply on a large surface; a cached mesh or pipeline prefix would hold a second copy of the mesh in the 32-bit heap MEMFS already shares, and make undo depend on invalidation. The heap's size is the half of this a higher ceiling or a 64-bit build would change (5.13); measure again there before reopening.
  • Code from AGPL-3.0 scan tools — ScanRuler and its STEP importer meshStep are AGPL-3.0, which this MIT library cannot take in any form: no copying, porting, translating or paraphrasing of their code, tests, shaders, guides or data. The scan metrology section (then §7, closed November 2026) was listed from their published feature descriptions only; an agent implementing an item of that kind (9.9 is one) works from the literature it cites and from meshio++'s own kernels, does not open their source, and does not use their output files as fixtures. The same rule as for Triangle, TetGen and Gmsh above, without the separate-process exception, since nothing there needs to run them.

  • Booleans of a scan with fitted solids — subtracting a measured cylinder from a scan, or adding a modelled body to it (roadmap 7.8.7, closed November 2026), waits on surface booleans, which a spike measured and kept out: exact in topology, but rounding its output to doubles left most curved results self-intersecting and coplanar contact unhandled (findings). A fitted solid reaches a CAD kernel as STEP (write_step_elements), where the boolean is the kernel's. If a boolean backend passes the spike's criteria (9.1), this is its first use and the entry is removed.

  • An interactive parametric CAD modeller — a sketch constraint solver, a feature history with rebuilds and rollback, face fingerprints that survive upstream edits, thread and insert catalogues: a CAD application, not a mesh library. meshio++ fits and writes geometry (fit_cad, 7.6.5) and may build a solid from a scripted list of profile features through the CAD provider (7.7.4); editing that solid belongs to the CAD tool that opens the STEP file.

  • Terminal rendering stays software, dependency-free and core-only — no GPU, no windowing stack, no new dependency and no audio library; it is a preview, not a replacement for Polyscope or vtk.js, and does not try to match their fidelity, lighting or picking. Python calls the core or raises by name (the remesh_volume precedent: a NumPy edge-function rasterizer would be slow and double the surface that has to stay byte-identical). Vector output is unchanged: SVG, TikZ and glTF keep the painter's algorithm and their byte-pinned twins, and their colormap tables are only ever extended. The interactive loop is not a library feature (raw mode, signals and the event loop live in the CLI layer; the library exposes frames and encoders, never a tty, and WASM and the C API carry frames and text only). --follow polls modification times and does not use inotify or kqueue. The soundtrack is generated, played by an external player and silent unless asked for, in CI and over SSH. See terminal rendering.


Suggested sequencing ​

Open work only; what shipped is in CHANGELOG.md.

  1. Correctness debts (§1) — the refusal of 1.1.1 first: it is days of work and ends a silent misread. One thing elsewhere has the same urgency though it is not data loss: the licence texts missing from the published MMG and Netgen add-on wheels (4.8.1.1).
  2. Format reach (§2) — the checks against the vendor tools, as licences or files the tools wrote appear (recording a run). Depth before breadth in what is new, smallest first: the series writers that only need wiring (2.3.4: MED, UNV, then Gmsh, Tecplot, EnSight, CGNS), writing the EnSight parts that were read (2.3.3.1), then CGNS's boundary conditions and structured zones (2.3.1.1, 2.3.1.3) and Exodus's globals (2.3.2.1). Of 2.4, the shared netCDF helper (2.4.3.1) and the ZIP container (2.4.5.1) are prerequisites worth having on their own; Fluent's case files (2.4.1) and the coastal trio (2.4.3) come first among the formats.
  3. The OSS-Fuzz submission (3.1) and registration (4.1) — calendar-bound, so start them early; SECURITY.md (3.14.1) before the submission. Among the registries, conda-forge (4.1.1), Blender (4.1.4) and the container image (4.1.8) need no decision; CRAN (4.1.2) and Julia (4.1.3) need the build question settled first. The dataset manager's thumbnails (4.6) can run beside them.
  4. Tooling that guards everything after it (§3) — in this order, each days of work: dependency updates and pinned actions (3.14.2, 3.14.3), clang-format and the documentation checks (3.8, 3.16), coverage on pull requests and Hypothesis in the ordinary legs (3.11.1, 3.11.2), the compiler cache (3.17.2), the warning option and its first count (3.6.1, 3.6.2), ThreadSanitizer (3.9.1), the C API abidiff gate (3.13.1). Then the longer ones: warnings to zero (3.6.3), CodeQL (3.10.1), typed Python (3.7), the round-trip fuzz target (3.12.1, after 3.1), the Clang-Tidy stages (3.5) and the free-threaded wheel (3.15.1, after 3.9.1). Code cleaning (3.4) runs alongside everything, in the order SonarCloud's duplication report gives.
  5. Performance (§5) — the missing rows first (5.1.2), since every other item shows its numbers on them; then the wheels' two checks (5.2), the WASM bulk copy with its benchmark (5.9), raising the browser's memory ceiling (5.13.1), and the measurements that can each close their item (5.12, 5.14, 5.15); decimate_volume (5.1.4), optimize_volume (5.5.2) and the sequence reads (5.11) by measured payoff.
  6. Split Python packages on Windows (4.7) and the CLI plugins (4.8) — the licence texts (4.8.1.1) at once; the static-runtime measurement (4.8.1.2) decides the shape of both the plugin archive and the Windows CLI flavour (4.7.2); the MSVC library recipe (4.7.1) is independent.
  7. Bindings and front ends — Rust (4.2) with the C-header parity guard, which Julia and R gain from too; then .NET (4.11). The Jupyter widget (4.12) and the VS Code preview (4.13) share the viewer's build and the rebuild of its embedded page (7.3). Interop phases 2 and 3 (4.3, 4.10) are days each.
  8. The chatbot (4.4) and the embeddable MCP server (4.9) — the schema generator (4.4.2) first, since both need it; then 4.9's design pass before any native code, since it decides whether the Python server's sandbox and sanitizer move into the core.
  9. Code duplication (§6) — no open item; rerun tools/dup_report.py alongside everything, like 3.4, and list what it finds there.
  10. SonarCloud findings (§7) — the next scan's counts first (7.1), then the coverage gate (7.2); the viewer's rebuild check (7.3) with the next WebAssembly build.
  11. Operations (§9) — the boolean backend's measurement first (9.1.1, 9.1.2), since it decides a non-goal and two parked features; then the small ones a user notices missing (mass properties 9.4, ray casting and point location 9.3, point-cloud conditioning 9.9, the IGA list 9.8), then subdivision and elevation (9.2, 9.5), the partitioner providers (9.7) and repeated green refinement (9.6.1). Only 9.10.2 and 9.6.2 wait on a decision.
  12. Data model (§8) — design passes only until one says otherwise. The two whose recommended form is a convention and days of work, and which close losses already listed on format pages: complex pairs (8.1) and the structured annotation (8.6). Strings (8.2) before hierarchy (8.5), which uses them. Integration-point data (8.3) is the largest and the one most formats wait on (2.3.6.1, MED ELGA, the result readers); side data (8.4) unblocks 2.3.1.4 and 2.3.2.3. The format items that wait on each say so.

Released under the MIT License.