-
Notifications
You must be signed in to change notification settings - Fork 0
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
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:
-
Fail-Loudly compliance has two breached perimeters in the Bevy adapter.
Err(_) => Default::default()inderived_state.rs:50andkinematic_propagation.rs:204silently substitutes all-zero / identity state for failed conversions, exactly the silent-numerical-wrongness mode CLAUDE.md says must never happen. Plus sixwarn!()-and-continue sites ingravity_controls.rs, GJ integration, leap-second table lookups, and component validation that auto-correct misconfigurations. -
The typed-quantity facade has one validation gap at the insertion-time boundary.
FrameTransform::from_matrix(self.t_struct_body)inastrodyn_bevy/src/lib.rs:1221is the documented boundary lift, but the underlying constructor's orthonormality check isdebug_assert!— silent in release builds. The exempt site is correct, the validation strength is wrong. -
Multi-planet parity is the largest single Tier 3 coverage gap. Seven of 25
KNOWN_PARITY_GAPSentries are blocked on the same architectural constraint (<P = Earth>fixed in the bridge layer); closing issue #389 unblocks all seven at once. -
Release engineering is unprepared for 1.0. No MSRV declared, no
NOTICEfile 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. -
Documentation has an in-repo / wiki imbalance.
docs/has onlyJEOD_invariants.md; the type-system primer, contributor on-ramp, and invariant-tagging how-to live in the wiki. Per-crateREADME.mdfiles 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):
- Fix the two
Err(_) => Default::default()sites in the Bevy adapter (≤2 hours) — these are silent numerical wrongness today. - Add
rust-version,NOTICE, and fixture metadata (~1.5 hours) — clears the path to 1.0. - Triage
KNOWN_PARITY_GAPS(likely 3–4 stale entries already have landed parity wrappers; cleanup is mechanical). - Promote the type-system primer and invariant-tagging how-to from the
wiki into
docs/(~half day).
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).
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.)
| 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 |
| 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 |
#501 — closed 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 |
| 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 |
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 --duplicatesshows 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 — nounsafeblocks anywhere incrates/orsrc/. - 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.
[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_bevy ↔ astrodyn_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).
[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.
[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.
[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.
[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.
[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.
[High — H-01] Two breached perimeters in the Bevy adapter:
-
crates/astrodyn_bevy/src/systems/derived_state.rs:50—Err(_) => elements.0 = Default::default()(all-zero orbital elements). Triggered byOrbitalError::{InvalidMu, DegenerateOrbit, KeplerConvergence}. A circular orbit at t=0 (common in test fixtures) is exactlyDegenerateOrbit, 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:204—Err(_) => Default::default()returns(DQuat::IDENTITY, ZERO)for the parent state when the query fails. A child entity attached to a parent missingRotationalStateC/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, 417 —
validate_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.
[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.
[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 ↔ JEOD ∧ runner ↔ bevy ⇒ bevy ↔ 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.
[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.
[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.
[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.
[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.
[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.
[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.
[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.
[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.
[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.
[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.
[Info] All 8 pitfalls from CLAUDE.md "Common Pitfalls" have inline source comments at the relevant code sites:
- Trick SIM working directory —
xtask, generate script. -
RefFrameStaterelative-to-parent —astrodyn_frames/src/ref_frame_state.rs:21,44,57. - Left-transformation quaternion —
JEOD_INV: RF.07tags. - Gravity excludes integration-frame self-acceleration —
astrodyn_dynamics/src/forces.rs. -
MassProperties.inertiabody-frame + parallel-axis —astrodyn_bevy/src/mass_tree.rs:7,596,1007and test inastrodyn_quantities/tests/inertia.rs:73. -
DynBodythree frames — handled by the<F>phantom on typed states +IntegOrigin. - Trick DRAscii silent drop — CLAUDE.md "Common Pitfalls" entry.
- Geodetic longitude at poles —
astrodyn_math/src/geodetic.rs:99–105andtier3_sim_ned.rstolerances.
Of these, only #7 (Trick DRAscii) and #8 (geodetic poles, see Axis 10 M-05) have a documentation gap — the others are well-annotated.
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). |
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.
-
scripts/check_no_bypass_deps.sh— Structural three-layer architecture enforcement at theCargo.tomllevel. Caught no regressions during the audit because the rule has been respected; that is the value. -
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. -
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. -
crates/astrodyn_verif_parity/tests/parity_coverage.rs— The parity-superset invariant that asserts every Tier 3 topic has either abevy_parity_*wrapper or an explicitKNOWN_PARITY_GAPSentry. This is the mechanism that makes the transitivity argument (runner ↔ JEOD∧runner ↔ bevy⇒bevy ↔ JEOD) auditable. -
No
target-cpu=native, no FMA, no Rayon in release profile (Cargo.toml:166–177). This is what makesbevy_parity_*bit-identity achievable. The intent is documented in the profile comment; preserve both the constraint and the comment. -
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. -
excludelist in workspace rootCargo.toml(trick/, docs/, scripts/, tests/, xtask/, .github/, CHANGELOG.md). Keeps the publishedastrodyncrate lean. Verify the list grows in lockstep with new top-level directories. -
Per-crate
[dev-dependencies]discipline — verification crates are dev-deps where they need to test integration with physics crates; production deps go throughastrodyn. Thecheck_no_bypass_deps.shscript enforces this structurally. -
JEOD invariant
// JEOD_INV: XX.YYsource 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. -
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. -
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. -
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.
-
Fix H-01 — Replace
Err(_) => Default::default()incrates/astrodyn_bevy/src/systems/derived_state.rs:50andcrates/astrodyn_bevy/src/kinematic_propagation.rs:204withunwrap_or_else(|err| panic!("..."))patterns naming the entity, the failing function, and the fix. ~2 hours. -
Fix H-04 and H-05 — Add
rust-versionto[workspace.package]; createNOTICE.mddocumenting NASA JEOD attribution. ~1 hour. -
Fix M-01 — Switch
astrodyn::FrameTransform::from_matrix(self.t_struct_body)atcrates/astrodyn_bevy/src/lib.rs:1221to the validated variant. ~30 min. -
Triage M-08 — Run
cargo test --test parity_coverageand remove any staleKNOWN_PARITY_GAPSentries. ~1 hour.
-
Resolve H-02 — Promote the six
gravity_controlswarn!()auto-corrections toassert!()(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. -
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. -
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 → panicupgrade plus a short migration note in the user-facing changelog. -
Address M-05 — Add polar-singularity docstring to
compute_body_geodetic_typedand the geodetic state type. Small. -
Address M-09retired — non-Rust-crate docs live in the wiki by project convention (reaffirmed 2026-05-14); #501 closed wontfix. Address M-10 — Update per-crateREADME.mdstubs to include "When to use" / "Key concepts" sections (Rust-crate docs are the in-repo exception to the wiki convention).
-
Address M-06 — Expand DB.07 / DB.08 catalog cells to explain the architectural reason force/torque collection is unconditional.
-
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. -
Address M-11 — Append JEOD version/commit metadata to
fixtures.meta.jsonalongside*.binfiles; update theextract_*binaries to emit this metadata on regen. -
Address M-12 — Either machine-enforce the baseline-tolerance PR-comment requirement, or add a nightly baseline-drift report.
-
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.
- 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.