# Invariants

These are the narrow rules the code upholds, one per row, each with the code that implements it and the test that guards it. A change that breaks a rule fails the test named beside it; a row marked "no test" relies on review. [Principles](<https://docs.femforge.io/contributing/principles/index.md>) covers the broader design rules these serve.

## Geometry and mesh

| Rule | Code | Test |
| --- | --- | --- |
| A multi-body STEP is OCC-fragmented into one conformal decomposition, so touching bodies share interface faces and nodes. | `src/femforge/meshing/mesher.py:162-174` | `tests/meshing/test_mesher_2d_integration.py::test_boundaries_are_curve_groups_named_by_adjacency` |
| Fragmenting drops STEP names, so each child gets back the name of its smallest-mass input, and an air-magnet overlap belongs to the magnet. | `src/femforge/meshing/mesher.py:275-301` | `tests/meshing/test_mesher_2d_integration.py::test_step_faces_keep_their_labels_and_become_body_groups` |
| gmsh writes only entities in a physical group, so the boundary partition covers every face exactly once. | `src/femforge/meshing/mesher.py:307`, `src/femforge/meshing/boundary.py:98-129` | `tests/meshing/test_boundary_groups.py::test_partition_is_total_every_face_covered_exactly_once` |
| A boundary group is named `<A>__<B>` for an interface, with the body names sorted, `<BODY>__EXT` for an external shell and `<BODY>__AXIS` on the symmetry axis of a 2D model. | `src/femforge/meshing/boundary.py:13-20`, `src/femforge/meshing/boundary.py:112-118` | `tests/meshing/test_boundary_groups.py::test_two_box_partition_yields_interface_and_externals` |
| The mesher always emits `Mesh.RecombineAll = 0`, because WhitneyAVSolver needs tetrahedra and a persisted gmsh options file can turn recombination on. | `src/femforge/meshing/gmsh_options.py:92-94` | `tests/meshing/test_gmsh_options.py::test_recombine_all_is_pinned_off` |
| The global element size applies first, then each region takes the finest `EntityTag.mesh_size` among its tags. | `src/femforge/meshing/mesh_sizing.py:30-44`, `src/femforge/meshing/mesh_sizing.py:62-64` | `tests/meshing/test_mesh_sizing.py::test_resolve_finest_requested_size_wins` |
| `MeshConfig.deterministic` pins gmsh to one thread, whatever `num_threads` says, so the same STEP and sizes yield the identical mesh. | `src/femforge/meshing/config.py:27-34` | `tests/meshing/test_gmsh_options.py::test_deterministic_pins_meshing_to_one_thread` |
| ElmerGrid renumbers the groups, so `reconcile_ids` rewrites each group's id from `mesh.names` before the sif targets it. | `src/femforge/elmer/mesh_names.py:64-83`, `src/femforge/run/driver.py:617-623` | `tests/elmer/test_mesh_names.py::test_reconcile_overwrites_gmsh_tags_with_elmer_ids` |
| The axisymmetric frame puts the radial direction on X and the symmetry axis on Y, and a section that reaches X \< 0 raises, since Elmer rejects it silently. | `src/femforge/common/frame.py:83`, `src/femforge/geometry/section.py:121` | `tests/geometry/test_section.py::test_a_section_leaking_below_x_zero_raises` |

## Solver input

| Rule | Code | Test |
| --- | --- | --- |
| A body's boundary conditions project onto every surface group the body borders, and an `external_only` condition lands only on an external shell. | `src/femforge/elmer/sim.py:410-422` | `tests/elmer/test_sif_smoke.py::test_fixed_temperature_reaches_only_the_board_surface` |
| A surface group gets one merged Boundary block, because Elmer honors a single BC section per boundary element. | `src/femforge/elmer/sim.py:377-397` | `tests/elmer/test_sif_smoke.py::test_one_boundary_block_per_group_with_conditions_merged` |
| Magnetic physics pins the far field to zero on external shells: `AV {e}` in 3D, the nodal `Potential` in 2D. | `src/femforge/elmer/sim.py:427-435` | `tests/elmer/test_sif_smoke.py::test_axisymmetric_2d_far_field_pins_potential_not_av` |
| Each body's material must emit the union of the `required_material_keys` of the solvers acting on it; a `conductor_only` solver's keys apply only to conductors. | `src/femforge/elmer/sim.py:143-160` | `tests/elmer/test_sif_validation.py::test_missing_required_material_property_raises` |
| A magnet's `Magnetization` components go on its own Material block, which WhitneyAVSolver reads, never on a Body Force. | `src/femforge/elmer/sim.py:445-468` | `tests/elmer/test_sif_smoke.py::test_magnet_body_gets_magnetization_on_material` |
| Magnetostatics solves with BiCGStabl of degree 4 and no preconditioning, since the gauge-non-unique AV system stalls under ILU. | `src/femforge/elmer/config/electromagnetism/magnetodynamics.py:55-57` | `tests/elmer/test_solvers.py::test_magnetodynamics_block_is_byte_compatible` |
| StatCurrent runs on conductor bodies only and solves Direct with umfpack; dielectrics take a second equation without it. | `src/femforge/elmer/config/electromagnetism/stat_current.py:44-60`, `src/femforge/elmer/sim.py:236-244` | `tests/elmer/test_sif_current.py::test_stat_current_solver_defaults_to_direct_umfpack` |
| Partitioning adds halo elements only for the coil stack, because halos skew the force lumping of partition-cut bodies. | `src/femforge/elmer/config/common/physics_config.py:91-93` | `tests/elmer/test_solvers.py::test_needs_halobc_only_for_the_coil_stack` |
| The sif writer reproduces pyelmer's output text and always writes LF line endings. | `src/femforge/elmer/sif.py:1`, `src/femforge/elmer/sif.py:393-396` | `tests/elmer/test_sif_writer.py::test_case_sif_matches_reference` |

## Units and config

| Rule | Code | Test |
| --- | --- | --- |
| A material property stays a pint quantity until `to_elmer` converts it to its Elmer unit and strips it to a float. | `src/femforge/materials/properties.py:59-63`, `src/femforge/elmer/sim.py:162-167` | `tests/elmer/test_sif_validation.py::test_leaked_pint_quantity_raises` |
| A physical field's docstring opens with `` `glyph` `[unit]`: meaning ``, and pint parses the unit. | `src/femforge/meshing/config.py:21-22` | `tests/test_field_docstrings.py::test_physical_field_docstring_format` |
| `Vec3` compares and hashes its exact components; `Direction` normalizes and rounds to 12 digits, so scaled copies compare equal. | `src/femforge/common/vector.py:122-142`, `src/femforge/common/vector.py:207-228` | `tests/vector/test_vec3.py::test_direction_is_normalized_and_scaled_copies_compare_and_hash_equal` |

## Runs and artifacts

| Rule | Code | Test |
| --- | --- | --- |
| Execution settings never enter the run identity: the backend, the mesher, the rank count, retention and telemetry. | `src/femforge/run/driver.py:128` | `tests/run/test_driver.py::test_driver_mpi_processes_is_not_part_of_the_run_identity` |
| `cache_key` hashes the config without `meta`, the STEP path and `physics.overrides`, plus the STEP key, the resolved solver stack and `RUN_PRODUCER_VERSION`, so overrides equal to the preset leave it unchanged. | `src/femforge/run/kinds.py:135-147` | `tests/run/test_manifest.py::test_cache_key_ignores_overrides_identical_to_the_preset` |
| Every artifact key hashes the kind name, its producer version and the spec without its `HashExclude` fields at any depth. | `src/femforge/artifacts/keys.py:91-105` | `tests/artifacts/test_keys.py::test_keys_are_stable_and_ignore_hash_excluded_fields_at_any_depth` |
| The mesh key hashes the `MeshSpec`, so materials' properties, conditions, physics and metadata never move it. | `src/femforge/run/kinds.py:108-132` | `tests/run/test_kinds.py::test_mesh_key_ignores_everything_that_only_changes_the_solve` |
| An artifact exists exactly when its `artifact.json` sidecar does, so the store writes the sidecar last, inside the staging directory, before the rename. | `src/femforge/artifacts/sidecar.py:19-20`, `src/femforge/artifacts/local.py:242-245` | `tests/artifacts/test_local_store.py::test_put_from_staging_prunes_unlisted_files_and_commits_atomically` |
| A record's `name` sits beside its key and is always derived from the key, never read from input. | `src/femforge/artifacts/sidecar.py:80-81`, `src/femforge/artifacts/sidecar.py:107-110` | `tests/artifacts/test_sidecar.py::test_record_name_is_derived_and_survives_a_round_trip` |
| Publishing is first-wins: the losing publisher's rename fails and its staging copy is discarded. | `src/femforge/artifacts/local.py:36-38`, `src/femforge/artifacts/local.py:244-249` | `tests/artifacts/test_local_store.py::test_two_publishers_racing_on_one_key` |
| A producer holds a heartbeat claim; a claim without a heartbeat for 120 s goes stale and another claimant breaks it. | `src/femforge/artifacts/lock.py:26-29`, `src/femforge/artifacts/lock.py:84-91` | `tests/artifacts/test_lock.py::test_a_claim_without_heartbeats_goes_stale_and_is_broken` |
| The default retention removes only store-sourced inputs, and no policy removes the manifest or the result. | `src/femforge/run/retention.py:100`, `src/femforge/run/retention.py:338-339` | `tests/run/test_retention.py::test_default_policy_removes_nothing_unless_cached` |
| Telemetry is append-only: each append writes a new shard per partition, and each process journal flushes every record before parquet sees it. | `src/femforge/telemetry/hive.py:149-162`, `src/femforge/telemetry/journal.py:146-155` | `tests/telemetry/test_hive.py::test_each_append_writes_a_distinct_shard` |

## Results

| Rule | Code | Test |
| --- | --- | --- |
| An MPI solve's scalars are read from rank 0 and never summed, since every rank writes the globally reduced row. | `src/femforge/run/parse.py:98-122` | `tests/run/test_parse.py::test_parse_mpi_rank_dats_read_rank_zero` |
| An axisymmetric result scales Elmer's per-radian forces by 2π, reports only the axial component and keeps `raw` verbatim. | `src/femforge/run/magnetic.py:39-41`, `src/femforge/run/parse.py:214-217` | `tests/run/test_parse.py::test_parse_axisymmetric_revolves_the_typed_vectors` |
| The field display's automatic range is the union of every body's own robust range, hidden or styled bodies included. | `src/femforge/viz/field_display.py:369-410`, `src/femforge/viz/field_display.py:790-792` | `tests/viz/test_field_display.py::test_the_color_range_covers_every_body_however_it_is_shown` |
| A field line ends at its first sharp reversal, where the cosine between consecutive steps drops below -0.5. | `src/femforge/viz/field_lines.py:222-259` | `tests/viz/test_field_lines.py::test_a_line_ends_at_its_first_sharp_reversal_and_a_straight_one_stays_whole` |

## Workbench

| Rule | Code | Test |
| --- | --- | --- |
| Field lines follow the shown field when it has three components, else the magnetic flux density. | `packages/femforge-workbench/src/femforge_workbench/panels/field_viewer.py:993-1003` | `tests/workbench/test_field_viewer.py::test_the_field_lines_follow_the_shown_vector_field_and_fall_back_to_the_flux_density` |
| A page has one VTK.wasm mirror: a panel that switches to the mirror moves every older mirroring panel to streaming. | `packages/femforge-workbench/src/femforge_workbench/panels/scene.py:523-528`, `packages/femforge-workbench/src/femforge_workbench/panels/scene.py:677-682` | `tests/workbench/test_scene.py::test_the_newest_panel_takes_the_mirror_and_the_older_ones_stream` |
| Mirrored polydata carries 32-bit cell arrays, because the wasm32 runtime misreads 64-bit ids. | `packages/femforge-workbench/src/femforge_workbench/panels/scene.py:221-230` | `tests/workbench/test_scene.py::test_the_mirror_gets_cell_arrays_its_32_bit_runtime_reads` |

## Packaging and imports

| Rule | Code | Test |
| --- | --- | --- |
| `import femforge` stays under 60 ms and loads no femforge subpackage, pydantic, pint, numpy, loguru or rich. | `src/femforge/__init__.py:3-8` | `tests/test_import_time.py::test_import_femforge_loads_no_heavy_dependency` |
| The first `SimulationConfig` loads no gmsh, build123d, polars, VTK, workbench or run stack, so `femforge.meshing` exports neither `Mesher` nor `MeshEntity`, which import gmsh. | `src/femforge/meshing/__init__.py:5-13`, `src/femforge/meshing/mesher.py:8`, `src/femforge/meshing/mesh_entity.py:3` | `tests/test_import_time.py::test_the_first_simulation_config_loads_no_cad_viz_or_run_stack` |
| A driver that meshes through `WorkerMesher` never loads gmsh in its own process, whether it meshes, produces a cached mesh or rebinds one. | `src/femforge/meshing/backend.py:204-205`, `src/femforge/meshing/firewall.py:28-34` | `tests/run/test_gmsh_firewall.py::test_a_worker_driver_never_loads_gmsh` |
| A lazy package's `__init__.pyi` is its only export list: `dir()` and `__all__` equal the stub's names. | `src/femforge/__init__.py:8` | `tests/test_public_api.py::test_dir_of_a_lazy_package_equals_its_stub` |
| A stub holds only one-dot relative imports and ASCII text, because `lazy_loader.attach_stub` rejects any other import level and reads the file in the locale encoding. | `src/femforge/__init__.pyi:1` | no test |
| A public callable with three or more parameters documents each one in a Google `Args:` block. | `src/femforge/meshing/naming.py:22-26` | `tests/test_public_api.py::test_a_public_callable_documents_its_arguments` |
| femforge and femforge-workbench carry the same version, and each pins the other at it. | `src/femforge/__init__.py:6`, `packages/femforge-workbench/pyproject.toml:3` | `tests/test_release.py::test_package_versions_match` |
