Skip to content

Audit 2026 05

Test User edited this page May 30, 2026 · 4 revisions

Astrodyn Codebase Audit — 2026-05

Wiki context. This is a comprehensive findings audit — a survey across architecture, type system, error handling, JEOD invariants, testing, performance, dependencies, and CI. For the capability-gap audit derived from the JEOD coverage matrix, see Audit-Findings. The two pages address different questions:

  • This page — "what's working / not-working / risky inside the astrodyn codebase as it stands today?" Findings are tagged H/M/L and filed as GitHub issues under label audit-2026-05.
  • Audit-Findings — "what JEOD capabilities are we still missing relative to v5.4?" Findings are bucketed into release blockers / docs / nice-to-have.

A finding in one page may map to a finding in the other (e.g., capability-gap "multi-planet scenarios" appears here as H-03), but the entry points are different and the prioritization is independent.

Date: 2026-05-13 Scope: entire workspace as of main @ 47b8902 Auditor brief: chief-engineer due diligence ahead of stabilization / 1.0 Companion tracker issue: #487


Executive Summary

Astrodyn is in strong shape. The structural guardrails are unusually tight for a project of this size and age: the three-layer architecture (astrodyn_* physics → astrodyn gateway → astrodyn_bevy / astrodyn_runner adapters) is enforced by two CI scripts (check_no_bypass_deps.sh, check_no_escape_hatches.sh) that close the structural loopholes a hand audit would otherwise have to grep for. The JEOD invariant catalog (docs/JEOD_invariants.md, 288 rows) is bidirectionally checked against source-side // JEOD_INV tags. Tier 3 cross-validation covers ~95% of in-scope physics with a documented baseline-freeze workflow. The typed-quantity facade (phantom frames, witness-gated quaternions, sealed traits) is intact at the public boundary; escape hatches are confined to documented insertion-time modules. unsafe_code is forbid-ed workspace-wide and no escapes exist.

The codebase is therefore not at structural risk. The findings below are the subtle class — the ones that will bite later, once the mental-model-owners are no longer at the keyboard. They cluster around five themes:

  1. Fail-Loudly compliance has two breached perimeters in the Bevy adapter. Err(_) => Default::default() in derived_state.rs:50 and kinematic_propagation.rs:204 silently substitutes all-zero / identity state for failed conversions, exactly the silent-numerical-wrongness mode CLAUDE.md says must never happen. Plus six warn!()-and-continue sites in gravity_controls.rs, GJ integration, leap-second table lookups, and component validation that auto-correct misconfigurations.

  2. The typed-quantity facade has one validation gap at the insertion-time boundary. FrameTransform::from_matrix(self.t_struct_body) in astrodyn_bevy/src/lib.rs:1221 is the documented boundary lift, but the underlying constructor's orthonormality check is debug_assert! — silent in release builds. The exempt site is correct, the validation strength is wrong.

  3. Multi-planet parity is the largest single Tier 3 coverage gap. Seven of 25 KNOWN_PARITY_GAPS entries are blocked on the same architectural constraint (<P = Earth> fixed in the bridge layer); closing issue #389 unblocks all seven at once.

  4. Release engineering is unprepared for 1.0. No MSRV declared, no NOTICE file documenting NASA JEOD attribution, fixture binaries (gravity coefficients, planet data) carry no JEOD version metadata. Estimated remediation: ~1.5 hours; the codebase is otherwise publishable.

  5. Documentation has an in-repo / wiki imbalance. docs/ has only JEOD_invariants.md; the type-system primer, contributor on-ramp, and invariant-tagging how-to live in the wiki. Per-crate README.md files are stubs. New contributors will read the code first.

What's notably strong (do not refactor away): the two CI scripts that police architectural boundaries, the bidirectional invariant-coverage test, the bit-identity parity-superset invariant between astrodyn_runner and astrodyn_bevy, the lto = "fat" + no-FMA release profile that preserves parity, the typed-quantity facade with sealed witness types, and the recipes/ + VehicleBuilder typestate that makes mission code read like physics.

Recommended near-term focus (in priority order):

  1. Fix the two Err(_) => Default::default() sites in the Bevy adapter (≤2 hours) — these are silent numerical wrongness today.
  2. Add rust-version, NOTICE, and fixture metadata (~1.5 hours) — clears the path to 1.0.
  3. Triage KNOWN_PARITY_GAPS (likely 3–4 stale entries already have landed parity wrappers; cleanup is mechanical).
  4. Promote the type-system primer and invariant-tagging how-to from the wiki into docs/ (~half day).

Method

Process. Three waves of structured Explore-agent surveys followed by synthesis.

  • Wave 1 (parallel): six packets covering axes 1–20, each with a ≤1500-word findings budget formatted as [severity] axis | finding | evidence | action.
  • Wave 2 (parallel): five deep dives selected by signal-×-blast-radius from Wave 1, each with a ~2000-word budget: (1) typed-quantity facade integrity, (2) Fail-Loudly compliance, (3) JEOD invariant catalog accuracy, (4) Tier 3 coverage and parity-gap triage, (5) release engineering and publishability.
  • Wave 3 (this document): synthesis, dedup, severity sort, GitHub issue filing.

Out of scope. No code changes (not even typo fixes — preserves audit signal). No CI workflow modifications. No PR triage of issues already open. No JEOD-source verification (the catalog cites ../jeod paths but the audit ran without that checkout; spot-checks relied on the catalog text and the Rust enforcement site).

Severity scale.

  • Critical — silent numerical wrongness, broken non-negotiable from CLAUDE.md, security issue, or release-blocker. Demands fixing before next release.
  • High — design flaw with a real failure mode in future work, missing guardrail with a credible regression path, or pre-1.0 blocker.
  • Medium — API friction, accumulated debt, weak test, documentation gap that costs a future contributor a measurable amount of time.
  • Low — nit, naming inconsistency, style, cleanup opportunity.
  • Info — observation worth recording, no action required.

Evidence convention. file_path:line_number matching the rest of the codebase (PR comments, the invariant catalog).


Findings by Severity

Critical

None. (See "Fail-Loudly" findings under High — two breached perimeters in the Bevy adapter produce silent zero/identity state for failed conversions. The audit deems these High rather than Critical because the failed-conversion condition is rare in non-pathological setups, but the silent-wrongness mode matches the Critical bar if triggered. Treat the boundary as fluid; act with the urgency of Critical.)

High

ID Axis Title Issue
H-01 7 Bevy adapter silently substitutes Default for failed derived-state conversions #488
H-02 7 Gravity controls auto-correct misconfigurations via warn!() instead of panicking #489
H-03 9, 18 Multi-planet parity gap blocks Tier 3 transitivity for 7 high-fidelity topics #490
H-04 13 No MSRV declared — workspace lacks rust-version field #491
H-05 13, 19 No NOTICE file documenting NASA JEOD source mirror attribution #492

Medium

ID Axis Title Issue
M-01 5 FrameTransform::from_matrix at spawn_bevy boundary uses debug_assert!-only orthonormality validation #493
M-02 7 Gauss-Jackson non-convergence accepts a degraded step with warn!() #494
M-03 7 validate_components_system logs warnings instead of panicking #495
M-04 7 Leap-second out-of-range silent fallback to boundary values #496
M-05 10, 12 Geodetic polar singularity not documented at public API (compute_body_geodetic_typed) #497
M-06 8 DB.07 / DB.08 partial-status catalog cells lack architectural-divergence rationale #498
M-07 9 Property-test coverage opportunity for sign-convention bug class (orbital elements, quaternion, time scales) #499
M-08 9 KNOWN_PARITY_GAPS may contain 3–4 stale entries already covered by landed parity wrappers #500
M-09 12 docs/ holds only JEOD_invariants.md; type-system primer and invariant tagging how-to live only on the wiki #501closed wontfix: contradicts the project convention that non-Rust-crate docs live in the wiki (reaffirmed 2026-05-14).
M-10 12 Per-crate README.md files are stubs (60–70 lines each, no "when to use" / "key concepts") #502
M-11 13, 19 Fixture binaries (*.bin) lack JEOD version / commit metadata for audit trail #503
M-12 17 Tier 3 baseline-diff gating depends on developer discipline; no automatic widening warning #508

Low

ID Axis Title
L-01 2 init_from_mean_anomaly re-exported with raw f64 parameters alongside typed sibling
L-02 10 Quaternion ScalarLast round-trip pathway not exercised by an explicit unit test
L-03 18 Three tier3_sim_dyncomp_run9 scenarios share identical ang-vel tolerances [3.558e-20, 4.447e-21, 7.116e-21] rad/s — verify intentional vs copy-paste
L-04 8 TM.03 (time-scale dependency ordering) may lack an explicit // JEOD_INV source tag
L-05 8 Invariant tag-comment descriptiveness threshold (15 alphanumeric chars) is cosmetic
L-06 19 astrodyn_ephemeris HTTPS-fetch behavior undocumented in crate README
L-07 11 One astrodyn_bevy/src/mass_tree.rs (2035 LOC) mirrors astrodyn_runner/src/simulation/mass_tree.rs (3205 LOC); intentional but undocumented at the top of each file

Info

Twenty-plus observations with no action required, recorded under "Findings by Axis" below. Key positives:

  • All four boundary scripts (check_no_bypass_deps, check_no_escape_hatches, invariant_coverage, parity_coverage) pass at audit time.
  • cargo tree --duplicates shows only upstream-induced version skew (half, itertools, snafu, syn from anise + bevy), none from workspace-direct deps.
  • unsafe_code = "forbid" is workspace policy and is honored — no unsafe blocks anywhere in crates/ or src/.
  • The typed-quantity facade's witness types (BodyAttitude<V>, NormalizedQuat, IntegOrigin) cannot be forged through any public constructor; sealed traits block external implementations.
  • The escape-hatch script's per-module exemptions (components.rs, lib.rs, simulation/{types,bodies,frame_attach,mass_tree}.rs) are each scoped to construction-time boundaries; per-step bypasses are not present.

Findings by Axis

Axis 1 — Three-layer boundary integrity

[Info] All three guardrails pass: check_no_bypass_deps.sh (gateway exclusivity), check_no_escape_hatches.sh (typed-quantity bypass policy), and the test for astrodyn_bevyastrodyn_runner peer symmetry. The dev-dep astrodyn_bevy → astrodyn_runner is correctly constrained to parity-test usage and does not leak the runner's API into the adapter's production surface.

No loopholes via [workspace.dependencies] aliasing, package renaming, or path-dep aliasing. The script's regex astrodyn_(dynamics|gravity| time|frames|interactions|math|quantities|atmosphere|ephemeris|planet) covers all current physics crates; adding a new physics crate would require updating this regex (a one-line, easily-reviewed change).

Axis 2 — API surface design (astrodyn gateway)

[Low — L-01] init_from_mean_anomaly is re-exported at src/lib.rs:219 alongside its typed sibling init_from_orbital_elements_typed. The raw-f64 variant is documented ("JEOD orbital-element / LVLH initialization paths used by mission init code and JEOD parity tests") but mission authors may reach for the simpler signature without realizing the typed sibling exists. Add a #[deprecated] or doc-link directing to the typed sibling, or remove from prelude so only the typed variant is discoverable.

[Info] The gateway re-export block in src/lib.rs:183–383 is exceptionally well-curated. Each re-export is justified by an active consumer and the curation criteria (§1–3 in the comment block) are documented. No orphan re-exports detected.

[Info] Public function signature consistency: compute_* / evaluate_* / accumulate_* / run_*_stage naming is consistent within each module. Cross-module, the verb selection follows JEOD's own terminology where possible (compute for derived state, accumulate for gravity, evaluate for atmosphere), which is a deliberate choice that helps cross-referencing.

Axis 3 — Computational independence

[Info] Production code path is clean. No code in src/, crates/astrodyn_*/src/ (excluding astrodyn_verif_* and fixtures / jeod_cc modules) reads from test_data/, jeod_inputs/, or parses .csv reference outputs. Tier 3 tests sampled (tier3_sim_dyncomp_run3, tier3_sim_drag_6dof, tier3_sim_lvlh, tier3_sim_shadow_2a_cooling) inject JEOD data only at t=0 (initial conditions) and at checkpoint comparison; the full Simulation::step() pipeline runs between checkpoints.

The single exception is tier3_sim_shadow_2a* (prescribed-motion tests) which sets position from JEOD CSV per step — but the position is set before the shadow-fraction computation and is not fed into gravity/atmosphere integration. This is an acceptable prescribed-motion fixture pattern, not a violation.

Axis 4 — Crate decomposition & cohesion

[Info] All 16 crate boundaries are justified. No mutual coupling. No disjoint-halves crates. astrodyn_dynamics (16.7K LOC / 28 files), astrodyn_bevy (18.7K LOC / 39 files), and astrodyn_runner (11.3K LOC / 20 files) are large but cohesive — each owns a distinct domain (integrators / Bevy adapter / arena harness).

[Low — L-07] crates/astrodyn_bevy/src/mass_tree.rs (2035 LOC) and crates/astrodyn_runner/src/simulation/mass_tree.rs (3205 LOC) are parallel implementations of the same JEOD mass-tree composition logic for different storage backends. The duality is intentional and correct per the three-layer rule, but neither file's module header explains the relationship. Add a 2–3 line cross-reference comment at the top of each.

Axis 5 — Type-system rigor

[Medium — M-01] astrodyn::FrameTransform::from_matrix(self.t_struct_body) at crates/astrodyn_bevy/src/lib.rs:1221. The site is at the documented insertion-time boundary (spawn_bevy) and the escape-hatch script exempts lib.rs, but the underlying constructor's orthonormality check is debug_assert! (crates/astrodyn_quantities/ src/frame_transform.rs:132–167) — silently no-op in release builds. self.t_struct_body originates from VehicleConfig as a pub DMat3 field with no builder-enforced validation, so a mission author who constructs VehicleConfig { t_struct_body: ..., .. } with a non-rotation matrix gets silent corruption of attitude. Replace with from_matrix_validated at this site (insertion-time has no per-step cost) and either expose a builder method or wrap the VehicleConfig field in a validated newtype.

[Info] Witness-gated constructors (BodyAttitude::from_witness, NormalizedQuat::new, FrameTransform::from_matrix_validated) cannot be forged — all public constructors validate the underlying invariant or accept a sealed witness. No from_unchecked / assume_* public constructors exist outside the documented kernel-boundary set.

[Info] All five escape-hatch-exempt modules are correctly scoped to construction-time: astrodyn_bevy/src/components/state.rs (14 lifts, all in From impls or from_untyped helpers), astrodyn_bevy/src/lib.rs (insertion-time spawn_bevy), and the four runner simulation/*.rs boundary modules. None contain per-step bypasses.

Axis 6 — Pipeline staging (AstrodynSet)

[Info] The 7-set decomposition is correctly applied. AstrodynSet ordering at crates/astrodyn_bevy/src/sets.rs matches PIPELINE_ORDER at src/pipeline.rs (TimeUpdate → EphemerisUpdate → Environment → Interaction → ForceCollection → Integration → DerivedState). RK4 sub-stages run as inner loops inside the Integration system, not as multiple schedule passes. DerivedState systems read only TranslationalStateC<P> and RotationalStateC (written by Integration); no back-writes to integration state.

Axis 7 — Error handling (Fail Loudly)

[High — H-01] Two breached perimeters in the Bevy adapter:

  • crates/astrodyn_bevy/src/systems/derived_state.rs:50Err(_) => elements.0 = Default::default() (all-zero orbital elements). Triggered by OrbitalError::{InvalidMu, DegenerateOrbit, KeplerConvergence}. A circular orbit at t=0 (common in test fixtures) is exactly DegenerateOrbit, so this is reachable, not pathological. Downstream consumers reading (a, e, i) = (0, 0, 0) see geometrically impossible state with no signal.
  • crates/astrodyn_bevy/src/kinematic_propagation.rs:204Err(_) => Default::default() returns (DQuat::IDENTITY, ZERO) for the parent state when the query fails. A child entity attached to a parent missing RotationalStateC / TranslationalStateC<P> propagates wrong frames silently.

Both must panic with diagnostics naming the entity, the failing function, and the fix. The CLAUDE.md sanctioned pattern is unwrap_or_else(|err| panic!("<context>: {err:?}. <fix>")).

[High — H-02] crates/astrodyn_gravity/src/gravity_controls.rs:242–315 contains six warn!() calls that auto-correct misconfigured gravity on-the-fly: degree=0 with spherical=false → flipped to true, gradient_degree > degree → clamped, etc. The intent mirrors JEOD's MessageHandler::error() semantics, but JEOD validates at C++ constructor time (single-pass); Bevy spawns entities at runtime across multiple systems, so a misconfiguration is silently corrected per spawn with only a log line. A mission swapping planet sources (Earth → Mars) and forgetting to update gravity degree gets silently-spherical gravity. Replace warn!() with assert!() + diagnostic, or move validation to a pre-pipeline asset loader gate.

[Medium — M-02] src/integration.rs:764–775 — Gauss-Jackson non-convergence emits log::warn!("GaussJackson integration step did not converge (position may be degraded)") and then completed = true; break;, accepting the degraded step. The trailing assert!(completed, ...) catches all-stages-fail but not per-step non-convergence. The accompanying bootstrap warning says "JEOD-faithful behavior, but long missions where bootstrap error compounds may want to review the integration setup" — explicitly acknowledges the silent-degradation mode. Either panic on per-step non-convergence or expose a configurable "panic on non-convergence" mode that defaults to on in release builds.

[Medium — M-03] crates/astrodyn_bevy/src/validation.rs:302, 417validate_components_system emits bevy::log::warn!("Entity {entity:?}: {error}") for missing required components. Validation is a dedicated system that runs separately from the integration pipeline, so a half-configured body propagates with logged but unaddressed errors. Replace with panic!() — validation should be a hard gate.

[Medium — M-04] crates/astrodyn_time/src/leap_second.rs:126–188 — four one-shot warn!() calls when TAI/UTC steps outside the leap-second table; the code silently uses the boundary value for all out-of-range epochs. A pre-1972 or post-table simulation gets incorrect time conversions. Either widen the leap-second table or panic on out-of-range.

[Info] debug_assert! usage is correctly scoped. ~40 calls across physics crates; sampled 10 — all are performance-oriented checks (sorted-array preconditions, mu-match between source and data, construction-time sanity checks). None are correctness invariants that would silently break in release.

[Info] unwrap() / expect() discipline is strong. Sampled 15 production-path unwraps; most are bounded by earlier query results. expect() messages follow the sanctioned pattern: noun phrase plus how-to-fix.

Axis 8 — JEOD invariant tracking integrity

[Medium — M-06] docs/JEOD_invariants.md:56–57 — DB.07 and DB.08 are marked partial with cell text "integration gated; force/torque collection is unconditional." The source tags (src/integration.rs:603–865) reference only the integration side (what is gated), not the architectural reason force collection is unconditional (Bevy systems run per-schedule, not per-entity-per-flag). A developer following the tag finds the gated half and assumes the catalog is wrong. Expand the catalog cell with one sentence on the Bevy schedule constraint.

[Low — L-04] TM.03 (time-scale dependency ordering) cites SimulationTime::advance as enforcement but the audit search did not find an explicit // JEOD_INV: TM.03 tag in that method. The invariant is structurally enforced (method body ordering), but a source tag would close a traceability gap and make the invariant_coverage bidirectional check still useful as a navigation aid.

[Low — L-05] The invariant-coverage test's "tag descriptiveness" check requires ≥15 alphanumeric chars in the source comment (tests/invariant_coverage.rs:334). Sampled tags are substantive (40–60 chars typical), so the threshold is cosmetic — not blocking real lint regression but also not protecting against a future contributor writing // JEOD_INV: XX.YY abc def ghi (16 chars but meaningless). Raise to ~30 chars or replace with a regex requiring at least one verb.

[Info] Sampled 10 catalog rows (DB.07, DB.08, DB.31, GV.04, RF.10, PF.03, TS.01, RF.07, AT.04, TM.03) end-to-end. Nine have accurate tag-to-code alignment. DB.31's catalog cell (frame-attach composite-body derivation) is dense — 1200+ chars in a single cell — but the density is unreducible given the multi-component chain. RF.10 (the three inertial-flavor phantoms) is even denser but unavoidable — it's the architectural foundation of the type-system refactor.

[Info] The catalog's status taxonomy (enforced / partial / deferred / n/a / structural) is well-defined and the bidirectional CI test is comprehensive (catalog→source, source→catalog, status validity, file existence, tag descriptiveness). The 2026-04-26 audit (referenced in commit history) re-validated all 68 runtime rows.

Axis 9 — Testing strategy & coverage gaps

[High — H-03] Seven of 25 KNOWN_PARITY_GAPS entries (crates/astrodyn_verif_parity/tests/parity_coverage.rs) are multi-planet scenarios blocked on the same architectural constraint: the bridge layer fixes <P = Earth> at scenario-init time, so non-Earth or multi-planet scenarios can't get a Bevy-side parity wrapper. These are: apollo8_frame_switch, apollo_mass_tree, apollo_trajectory, earth_moon, mars_orbit, mercury, planetary. They cover the highest-fidelity physics (110×110 Mars SH, Earth-Moon 3rd-body, Apollo lunar transfer). Bevy's gravity, 3rd-body, and frame-switch paths are untested for these scenarios — transitivity argument (runner ↔ JEODrunner ↔ bevybevy ↔ JEOD) does not hold here. Close via #389 (generic Planet dispatch in the bridge recipe layer) and extract parity wrappers in the same PR cadence.

[Medium — M-07] Property-test coverage is sparse. proptest is a dev-dep in 6 crates and proptest_round_trips.rs exists in astrodyn_quantities/tests/, but the high-value targets identified by the CLAUDE.md time_periapsis cautionary tale (sign-convention bugs) are not covered: orbital-element round-trip across the e ∈ [0, 1.1], i ∈ [0, π] parameter space, JEOD ↔ glam quaternion round-trip across non-trivial rotations, frame-transform round-trip for every typed-pair, geodetic round-trip at polar latitudes, time-scale conversion across leap-second insertion events. Each is a ~20-line proptest! macro that would catch a regression class no fixed unit test covers.

[Medium — M-08] KNOWN_PARITY_GAPS likely contains 3–4 stale entries. The recent commit log shows parity wrappers landed for dyncomp_run9 (May 12), drag_verif and drag_rot_verif (May 11), ref_attach (May 13). The parity_coverage test enforces that listed gaps must still lack a wrapper, so CI may already be flagging these. Either way, a cleanup PR removing landed entries restores the list's signal-to-noise.

[Info] Tier 3 coverage is 95% of in-scope physics — every AstrodynSet stage, every major interaction force (drag / SRP / gravity gradient torque / 3rd body), every integrator family, every derived state type (LVLH, NED, relative, geodetic), and mass-tree attach/detach. The gaps are architectural (multi-planet) or out-of-scope (analytical / structural / pure-math tests that don't fit the VerificationCase trait shape).

[Info] Tolerance hygiene is strong. Sampled 8 Tier 3 tests; all follow the error × 1.05 policy. Outliers (Mars 110×110 at 4m, Earth-Moon at 1m) are justified by chaotic sensitivity in high-order gravity fields or by being in KNOWN_PARITY_GAPS. Three tier3_sim_dyncomp_run9 scenarios share the same ang-vel tolerance literal — L-03 below.

Axis 10 — Numerical conventions

[Medium — M-05] crates/astrodyn_math/src/geodetic.rs:99–105 handles the polar singularity correctly (returns 0.0 for longitude at x_ellipse.abs() < 1e-10), but the public compute_body_geodetic_typed<P> function carries no docstring warning about the singularity. CLAUDE.md "Common Pitfalls" documents the 3.7e-6 rad/m sensitivity at 89.8° latitude — mission engineers calling the typed function from outside the crate will not see this. Add a # Polar singularity section to the doc comment.

[Low — L-02] Quaternion conversion paths are unit-tested for one non-trivial rotation (1.5 rad about Y at crates/astrodyn_math/src/quaternion.rs:208–217) but the ScalarLast intermediate pathway (q.to_scalar_last().to_glam()Quat::<ScalarLast, LeftTransform>::from(glam_q).to_scalar_first()) lacks an explicit two-step round-trip test.

[Info] RefFrameState relative-to-parent semantics are documented at the type definition site (crates/astrodyn_frames/src/ref_frame_state.rs:21, 44, 57) and frame_storage.rs:48. Consumers reach the global-frame state via frame_compute_relative_state_via_storage. No latent "treat as global" bugs found.

[Info] Left-transformation quaternion convention is tagged at multiple sites with // JEOD_INV: RF.07. Conversions at the boundary (JeodQuat::to_glam, Quat::from_glam) are concentrated and tested.

Axis 11 — Function complexity & long files

[Info] Files >2K LOC are cohesive subsystems, not god-modules: crates/astrodyn_bevy/src/systems/integration.rs:3097 (Bevy integration system + RK4 inner loop + frame propagation), crates/astrodyn_dynamics/src/mass_body.rs:2003 (mass tree + mass body + mass points), and the parity test crates/astrodyn_verif_parity/tests/bevy_parity_attach_detach_momentum.rs:4332 (comprehensive momentum-conservation regression suite). All justified.

[Low — L-07] As recorded in Axis 4.

Axis 12 — Documentation quality

[Medium — M-09] docs/ holds only JEOD_invariants.md (99 KB, 288 rows). The type-system primer, contributor on-ramp, and invariant tagging how-to are all on the GitHub wiki. Wiki content is not versioned with the code and is not visible offline / in cargo doc. Migrate the type-system primer, the JEOD-invariant tagging how-to, and a short contributor-on-ramp guide into docs/ (as docs/TYPE_SYSTEM.md, docs/INVARIANT_TAGGING.md, docs/CONTRIBUTING.md) and let the wiki link back rather than the other way around.

[Medium — M-10] Per-crate README.md files are stubs (60–70 lines each, project boilerplate). For 1.0 polish each should have a "When to use" and "Key concepts" section so a crates.io browser sees substance.

[Info] Public API docstrings are substantive where it matters. Sampled astrodyn::accumulate_gravity, astrodyn::integrate_body, astrodyn::VehicleBuilder, component types in crates/astrodyn_bevy/src/components/ — all explain why and when. #![deny(missing_docs)] at workspace level prevents stub docstrings from landing.

[Info] Examples (crates/astrodyn_bevy/examples/) are current and consistent. typed_mission.rs (the canonical example per CLAUDE.md), kepler_orbit.rs, apollo.rs, leo_drag.rs, multi_body_scenario.rs all use the modern VehicleBuilder typestate path. No API drift.

Axis 13 — Dependency & build hygiene

[High — H-04] No rust-version field in any Cargo.toml. The workspace targets edition 2021 + features like #[diagnostic::on_unimplemented] (Rust 1.78+) and uses bevy 0.18 (which itself requires Rust 1.80+). Users on older toolchains get cryptic compiler errors instead of a clean MSRV diagnostic. Add rust-version = "1.80" (or whichever value cargo +<old> check reveals) to [workspace.package].

[High — H-05] No NOTICE.md / NOTICE.txt at the repo root. crates/astrodyn_verif_jeod/test_data/jeod_inputs/README.md documents the JEOD source mirror as "verbatim" but does not state the upstream license, the NASA SRA / attribution requirements, or how MIT OR Apache-2.0 (astrodyn) composes with NASA's open-source license. Downstream consumers (mission crates, JPL teams) auditing supply chain will hit this gap. Create a root NOTICE.md documenting: upstream JEOD v5.4 license, NASA attribution, the fact that astrodyn reimplements (not vendors) the physics, and that the test_data/jeod_inputs/ tree is verification reference data only.

[Info] cargo tree --duplicates shows four version-skewed transitive deps (half, itertools, snafu, syn). All come from upstream (anise + bevy), not from workspace direct deps. No action.

[Info] Workspace [workspace.dependencies] are current: glam 0.30, bevy 0.18, uom 0.38, typenum 1.20, thiserror 2, criterion 0.5, pprof 0.14. All maintained.

[Info] unsafe_code = "forbid" is honored — zero unsafe blocks in crates/ or src/.

[Info] Path dependencies missing version = are all in [dev-dependencies] (where it's permitted) referencing astrodyn_verif_jeod_fixtures or other test-only crates. Not a publish blocker.

Axis 14 — Dead / orphaned code

[Info] All 13 #[allow(dead_code)] annotations sampled are justified (fixture reuse across test scenarios, marker types for type safety, intentional imports in examples). No stale suppressions.

[Info] extract_* binaries (6 total) are each referenced from a documented regen workflow. None orphaned.

[Info] astrodyn_verif_jeod_fixtures (5.4K LOC, 18 files, 0 tests) is consumed by 6 crates plus astrodyn_gravity::fixtures. No dead modules.

Axis 15 — Performance & numerical hot paths

[Info] Gravity Gottlieb algorithm (crates/astrodyn_gravity/src/spherical_harmonics_calc_nonspherical.rs) uses thread-local scratch (GottliebScratch) with lazy growth; no allocations in the per-degree-order inner loop. Coefficient layout is Vec<Vec<f64>> but accessed via precomputed offsets — not iterated naively, so cache behavior is fine.

[Info] RK4 integration has no redundant typed↔raw conversions per sub-step. crates/astrodyn_dynamics/src/integration.rs:72–115 works in raw DVec3 throughout; the typed boundary is at the call site only.

[Info] Bench harness in crates/astrodyn_gravity/benches/accumulate.rs correctly isolates setup from measurement (scratch allocated outside iter()).

[Info] Perf-baseline tracking is on main-only (5 repeats with mean/stdev, 365-day artifact retention). Trend-regression gating is not automated — this is a snapshot approach, not a trend approach. Acceptable; a future improvement is a budget-violation gate.

Axis 16 — Concurrency / determinism

[Info] Bit-identity between astrodyn_runner and astrodyn_bevy is preserved by (a) deterministic schedule ordering via AstrodynSet, (b) no Rayon / FMA / target-cpu=native (explicitly forbidden per Cargo.toml:166–168 comment), (c) lto = "fat" + codegen-units = 1 for cross-crate inlining without re-ordering f64 ops. Verified via bevy_parity_* test asserting f64::to_bits() equality at every step.

[Info] Read-only resources (SimulationTimeR, IntegrationDtR) are read concurrently across stages but never mutated mid-step. Mutations are confined to single systems within their owning stage.

Axis 17 — CI & tooling

[Medium — M-12] Tier 3 baseline-diff gating (crates/astrodyn_verif_jeod/src/bin/tier3_baseline_diff.rs) is enforced on refactor-only PRs but not on physics-change PRs (which require a PR comment with "physical justification" — manual, not machine-enforced). A physics change that worsens error within tolerance bounds will pass CI silently. The mitigation (parity wrappers asserting bit-identity vs runner) catches Bevy-side regressions but not runner-side ones. Either machine-enforce the comment via a label / required-checks-update or add a nightly job that runs the full Tier 3 suite and posts a baseline-drift report.

[Info] check_no_bypass_deps.sh regex covers all 10 current physics crates and is enforced on every PR. Adding a new physics crate requires updating this regex (a single-line, easily-reviewed change). No way past it via [workspace.dependencies] aliasing or Cargo features.

[Info] check_no_escape_hatches.sh policies six bypass constructors across the gateway and Bevy adapter. The #[cfg(test)] brace-depth tracker and propagating // allowed: comment handling are robust to multi-line generic patterns.

[Info] CI triggers (PRs + push to main only) match CLAUDE.md. The test-tier3-full and test-parity-trajectory-full jobs run on main-only to keep PR feedback under ~12 min.

[Info] xtask subcommands are coherent (regenerate-tier3, perf-baseline, publish). No half-finished subcommands. The publish flow validates topological order and supports --dry-run.

Axis 18 — Tier 3 drift envelopes & tolerance hygiene

[Low — L-03] tier3_sim_dyncomp_run9 has three scenarios with identical ang-vel tolerance [3.558e-20, 4.447e-21, 7.116e-21] rad/s literals. Likely copy-paste rather than independent measurement — verify the three scenarios actually converge to the same per-component error, or extract a shared constant.

[Info] Tolerance policy (error × 1.05 per component) is consistently applied. Outliers (Mars 110×110 at 4m, Earth-Moon at 1m) are justified inline in the test source.

[Info] Baseline-freeze workflow in crates/astrodyn_bevy/tests/README.md documents the refreeze process and the PR-comment requirement for tolerance widening.

Axis 19 — Reference data provenance & regen integrity

[Medium — M-11] crates/astrodyn_gravity/test_data/gravity/*.bin (committed binaries, ~1.9 MB total) and crates/astrodyn_planet/test_data/* (planet data) carry no JEOD version / commit metadata. The regen workflow (extract_grav_coeffs, extract_planet_pfixposn) is documented and deterministic given a JEOD checkout, but a downstream supply-chain auditor cannot verify "this ggm05c.bin came from JEOD commit X" without running the regen and comparing bytes. Append a fixtures.meta.json (or update the existing grav_coeffs.json) listing JEOD commit SHA, generation timestamp, and SHA-256 per file.

[Low — L-06] astrodyn_ephemeris HTTPS-fetch behavior (fetch feature, default-on) is undocumented in the crate README.md. The fallback chain (env var → manifest dir → user cache → GitHub fetch) is in code comments but not user-facing. Air-gapped users will discover it by failing to compile.

[Info] Trick reference-CSV regen flow (xtask regenerate-tier3 / Docker) is documented in CLAUDE.md and the per-test source comments. The Trick DRAscii silent-variable-drop pitfall is documented in CLAUDE.md "Common Pitfalls"; the generate_references.sh script does not have a column-count guard, but the audit considers this a documentation issue (caught by review of regenerated CSVs), not a runtime issue.

Axis 20 — JEOD pitfall awareness at code sites

[Info] All 8 pitfalls from CLAUDE.md "Common Pitfalls" have inline source comments at the relevant code sites:

  1. Trick SIM working directory — xtask, generate script.
  2. RefFrameState relative-to-parent — astrodyn_frames/src/ref_frame_state.rs:21,44,57.
  3. Left-transformation quaternion — JEOD_INV: RF.07 tags.
  4. Gravity excludes integration-frame self-acceleration — astrodyn_dynamics/src/forces.rs.
  5. MassProperties.inertia body-frame + parallel-axis — astrodyn_bevy/src/mass_tree.rs:7,596,1007 and test in astrodyn_quantities/tests/inertia.rs:73.
  6. DynBody three frames — handled by the <F> phantom on typed states + IntegOrigin.
  7. Trick DRAscii silent drop — CLAUDE.md "Common Pitfalls" entry.
  8. Geodetic longitude at poles — astrodyn_math/src/geodetic.rs:99–105 and tier3_sim_ned.rs tolerances.

Of these, only #7 (Trick DRAscii) and #8 (geodetic poles, see Axis 10 M-05) have a documentation gap — the others are well-annotated.


Risk Register

These are the regression scenarios the project should track as it approaches 1.0. Each names a specific failure mode, not a generic worry.

# Risk Likelihood Impact Mitigation in place Gap
1 A mission entity spawned without MassPropertiesC propagates with zero mass; integration produces NaN within seconds and downstream consumers see corrupt state. Medium High validate_components_system runs as a separate stage. Validation logs warnings instead of panicking (M-03).
2 A circular-orbit test fixture (e=0, i=0) triggers OrbitalError::DegenerateOrbit; the Bevy derived_state system substitutes zero orbital elements; a downstream consumer reading (a, e, i) = (0, 0, 0) accepts the value as correct. Medium High Tier 2 unit tests cover the conversion. The Err arm in the Bevy system silently defaults (H-01).
3 A multi-body Apollo mission lands silently with degraded accuracy because the Bevy adapter's 3rd-body gravity path was not validated against astrodyn_runner (which is JEOD-validated). Low (today, due to architectural gating) Critical (when unblocked) parity_coverage test enforces a gap-list entry until a wrapper lands. 7 multi-planet topics in KNOWN_PARITY_GAPS (H-03); transitivity argument doesn't hold for these until #389 closes.
4 A mission author constructs VehicleConfig { t_struct_body: <non-rotation matrix>, .. }; release builds silently propagate the non-orthonormal matrix into per-step attitude updates. Low High from_matrix's debug_assert! catches it in test builds. Release build has no validation at the insertion-time site (M-01).
5 A future contributor writes a new physics module that calls JeodQuat::from_array(...) thinking the scalar-first vs scalar-last convention is handled; the resulting rotation is transposed. Low (the escape-hatch script guards the gateway + adapter) High Sealed quaternion newtypes + escape-hatch script. The script doesn't cover physics-crate internals; an // allowed: line bypasses the check.
6 A JEOD upgrade changes a gravity coefficient file format; the extract_grav_coeffs regen silently produces different .bin files and committed test data is now inconsistent with the upstream we cite as the reference. Low Medium Regen workflow is documented. No JEOD version metadata in fixture binaries (M-11); audit trail is verbal.
7 A long-duration mission crosses a leap-second insertion event the table doesn't cover; time-tagged ephemeris queries shift by 1 second; trajectory cross-validation against future JEOD reference data drifts. Low Medium One-shot warning at the boundary. The warning is log::warn! (silent under default log config) and the code continues with the boundary value (M-04).
8 The KNOWN_PARITY_GAPS list accumulates stale entries (entries that already have a landed parity wrapper but weren't removed); future contributors deferring to the list misjudge what's covered. Medium (some stale entries are likely there today) Low parity_coverage test runs in CI and asserts gap-list entries genuinely lack a wrapper. Cleanup is mechanical but not automated (M-08).
9 A non-FMA, no-target-cpu invariant in Cargo.toml is silently broken by a future contributor adding target-cpu=native for benchmarking; bit-identity tests fail mysteriously the next time someone re-runs bevy_parity_*. Low Medium Comment block in Cargo.toml:166–168 explains why; no CI guard. A CI lint that greps for forbidden release-profile features would close this.
10 Stabilization era requires API contracts; without an MSRV declaration, a user on Rust 1.76 reports a cryptic build failure and the project has no policy answer. Medium (the first user with an old toolchain) Low None today. Declare MSRV (H-04).

Strengths Worth Preserving

These guardrails are exceptional and a future refactor should be careful not to unwittingly dismantle them. If a PR proposes to remove or weaken one of these, the author should be required to spell out what replaces it.

  1. scripts/check_no_bypass_deps.sh — Structural three-layer architecture enforcement at the Cargo.toml level. Caught no regressions during the audit because the rule has been respected; that is the value.

  2. scripts/check_no_escape_hatches.sh — Per-line awk-based policing of typed-quantity bypass constructors. The // allowed:-comment propagation logic handles multi-line generic patterns correctly. Removing the script in favor of "trust the reviewer" would be a serious regression.

  3. tests/invariant_coverage.rs — Bidirectional check (catalog ↔ source tags) that prevents the JEOD invariant catalog from drifting from the codebase. Without this the catalog would degrade silently into a documentation artifact.

  4. crates/astrodyn_verif_parity/tests/parity_coverage.rs — The parity-superset invariant that asserts every Tier 3 topic has either a bevy_parity_* wrapper or an explicit KNOWN_PARITY_GAPS entry. This is the mechanism that makes the transitivity argument (runner ↔ JEODrunner ↔ bevybevy ↔ JEOD) auditable.

  5. No target-cpu=native, no FMA, no Rayon in release profile (Cargo.toml:166–177). This is what makes bevy_parity_* bit-identity achievable. The intent is documented in the profile comment; preserve both the constraint and the comment.

  6. Witness-gated typed quantities (BodyAttitude<V>, NormalizedQuat<L, T>, IntegOrigin, FrameTransform<From, To>). Sealed-trait bounds prevent external code from forging witnesses; public constructors validate or accept a sealed witness. The pattern requires more code than alternatives but the safety guarantee is structural, not procedural.

  7. exclude list in workspace root Cargo.toml (trick/, docs/, scripts/, tests/, xtask/, .github/, CHANGELOG.md). Keeps the published astrodyn crate lean. Verify the list grows in lockstep with new top-level directories.

  8. Per-crate [dev-dependencies] discipline — verification crates are dev-deps where they need to test integration with physics crates; production deps go through astrodyn. The check_no_bypass_deps.sh script enforces this structurally.

  9. JEOD invariant // JEOD_INV: XX.YY source tags — 517 sites per the deep-dive count. They function as navigation aids (catalog → code) and as documentation (code → catalog). The density is intentional and useful; removing tags as "noise" would be a serious regression.

  10. docs/JEOD_invariants.md — 288-row catalog with status, JEOD enforcement mechanism, and Rust enforcement site. The bidirectional CI check makes this a live contract, not a dead doc.

  11. The Vehicle{Builder, Config} typestate — the typestate transitions make the compiler refuse to build a vehicle without state, mass, and integrator. New contributors will reach for this naturally; preserve the typestate ordering.

  12. Recipe modules (astrodyn::recipes::{earth, orbital_elements, vehicle, scenarios})earth::point_mass(), orbital_elements::iss(), vehicle::iss_mass() make mission code declarative. The recipe layer is the "good behavior path" that makes manual struct construction look out-of-place.


Recommendations & Roadmap

This sprint

  1. Fix H-01 — Replace Err(_) => Default::default() in crates/astrodyn_bevy/src/systems/derived_state.rs:50 and crates/astrodyn_bevy/src/kinematic_propagation.rs:204 with unwrap_or_else(|err| panic!("...")) patterns naming the entity, the failing function, and the fix. ~2 hours.

  2. Fix H-04 and H-05 — Add rust-version to [workspace.package]; create NOTICE.md documenting NASA JEOD attribution. ~1 hour.

  3. Fix M-01 — Switch astrodyn::FrameTransform::from_matrix(self.t_struct_body) at crates/astrodyn_bevy/src/lib.rs:1221 to the validated variant. ~30 min.

  4. Triage M-08 — Run cargo test --test parity_coverage and remove any stale KNOWN_PARITY_GAPS entries. ~1 hour.

This quarter

  1. Resolve H-02 — Promote the six gravity_controls warn!() auto-corrections to assert!() (or to a pre-pipeline validation stage). Requires a small design decision: do we accept runtime spawning of misconfigured gravity, or require validation at config time? ~half day.

  2. Address H-03 — Close #389 (generic Planet dispatch in the bridge recipe layer); extract parity wrappers for the 7 multi-planet topics in the same PR cadence. Largest single piece of work on the list.

  3. Address M-02, M-03, M-04 — Decide policy on the three "warn and continue" sites (GJ non-convergence, component validation, leap-second out-of-range). Each is a warn → panic upgrade plus a short migration note in the user-facing changelog.

  4. Address M-05 — Add polar-singularity docstring to compute_body_geodetic_typed and the geodetic state type. Small.

  5. Address M-09 retired — non-Rust-crate docs live in the wiki by project convention (reaffirmed 2026-05-14); #501 closed wontfix. Address M-10 — Update per-crate README.md stubs to include "When to use" / "Key concepts" sections (Rust-crate docs are the in-repo exception to the wiki convention).

Pre-1.0

  1. Address M-06 — Expand DB.07 / DB.08 catalog cells to explain the architectural reason force/torque collection is unconditional.

  2. Address M-07 — Add the five property-test recommendations (orbital element round-trip, quaternion convention, frame transform, geodetic, time-scale). Each is a ~20-line proptest! macro.

  3. Address M-11 — Append JEOD version/commit metadata to fixtures.meta.json alongside *.bin files; update the extract_* binaries to emit this metadata on regen.

  4. Address M-12 — Either machine-enforce the baseline-tolerance PR-comment requirement, or add a nightly baseline-drift report.

  5. Address Low-severity items as quality polish — L-01, L-02, L-03, L-04, L-05, L-06, L-07 are all single-PR cleanups.

Carry-forward / track but don't act now

  • Risk register #5 (escape-hatch script doesn't cover physics-crate internals). The current scope is the gateway + adapter, which is the right scope; widening would create false positives. Track as a "watch" item.
  • Risk register #9 (no CI guard preventing target-cpu=native). Add a small lint when convenient; not blocking.

End of report. Spot-check three findings end-to-end before relying on this document for planning; cross-reference the filed GitHub issues under label audit-2026-05.

Clone this wiki locally