Motivation
CLAUDE.md's JEOD Convention Rule exists because of a real near-miss: an agent guessed the time_periapsis → mean anomaly formula as M = 2π − n·t instead of M = n·t, producing 11,668 km of error against NASA flight data. The rule today reads:
When JEOD uses a field name whose meaning could be ambiguous (e.g., time_periapsis — is it time to periapsis or time since periapsis?), always read the JEOD C++ source to determine the convention. Do not guess or reason by analogy.
It's a cultural rule. An AI-assisted PR that ports a formula without reading the .cc can satisfy fmt + clippy + most tests and still be silently wrong — exactly the failure shape discussed for issue #517. The fix discussed there (a generic "every new formula needs a .cc:line citation" lint) was deferred because the citation grammar wasn't tractable.
This issue proposes a more tractable shape: a physics-equation catalog that mirrors the existing docs/JEOD_invariants.md ↔ // JEOD_INV: ↔ tests/invariant_coverage.rs mechanism, but for formulas instead of guard conditions. The catalog itself is the citation grammar.
Proposal
docs/JEOD_equations.md — a parallel catalog
One row per non-trivial formula we port from JEOD. Each row carries:
- Tag —
Section.Item, e.g. KE.03 (Kepler/anomaly conversions), RO.07 (rotation), GR.12 (gravity gradient), AT.02 (atmosphere), TM.05 (time scales), RF.04 (frame transforms).
- Equation / quantity — short prose name (e.g. "mean anomaly from time-since-periapsis").
- Formula — written in unambiguous form (LaTeX or plain ASCII), including sign and direction conventions explicitly.
- JEOD source —
models/.../*.cc:line reference, with the relevant function name. Example: models/dynamics/body_action/src/dyn_body_init_orbit.cc:218 dyn_body_init_orbit::apply().
- Our implementation — crate path + function (e.g.
crates/astrodyn_orbital/src/elements.rs:mean_anomaly_from_time_since_periapsis).
- Convention notes — anything that bit the original port or could bite a future one. Sign conventions, hand of rotation, scalar-first vs scalar-last, time-scale, frame.
- Divergence — where our Rust differs from JEOD's C++ (e.g. "we divide by mass at runtime; JEOD precomputes inverse_mass"). Parallel to the existing invariant catalog's divergence column.
Source-side tags
Mirror the JEOD_INV pattern:
// JEOD_EQ: KE.03 — M = n·(t − t_peri); never the 2π complement form
fn mean_anomaly_from_time_since_periapsis(n: f64, t_since_peri: f64) -> f64 {
n * t_since_peri
}
The tag goes at the formula site, not at the function signature, so derivations that compose two catalog entries can carry both tags inline.
tests/equation_coverage.rs — bidirectional CI gate
Parallel to tests/invariant_coverage.rs:
- Every row in
JEOD_equations.md must have at least one // JEOD_EQ: XX.YY tag in the workspace source.
- Every
// JEOD_EQ: XX.YY tag must reference a catalog row.
- Every row's JEOD source citation must be syntactically well-formed (
models/.../*.cc:<line> or models/.../*.hh:<line>); the test does not attempt to verify the citation against an actual JEOD checkout (CI runs without $JEOD_HOME, per CLAUDE.md), but the format check rules out garbage.
This is the citation grammar: rows in the catalog are the allowed citations, and tags are the allowed call sites.
Why a catalog and not a free-form citation lint
A "require .cc:line in any new formula" lint has two failure modes that the catalog avoids:
- What counts as a "formula"? A
v.normalize() call doesn't need a citation; a quaternion-from-Euler-angles construction does. A regex can't tell them apart; a catalog row makes the boundary explicit ("if it's in the catalog, it needs a tag; if it's not in the catalog, an author who thinks it should be opens a PR adding the row").
- Citation rot. A free-form
.cc:line comment goes stale silently when JEOD reorganizes. A central catalog can be re-grepped/re-verified in one place during a JEOD bump, the same way extract_* binaries are.
This shape also matches what already works: JEOD_INV solves an analogous problem and the team is fluent in the pattern.
Scope discipline
The catalog must not explode into an enumeration of every line of math. Inclusion criterion: the formula has a convention choice that could be guessed wrong and produce silently-wrong physics that passes shallow tests.
Candidates on day one (non-exhaustive — actual numbering chosen during implementation):
- Anomaly conversions (Kepler equation iteration, M ↔ E ↔ ν, sign of
time_periapsis)
- Quaternion ↔ Euler / DCM conversions (scalar position, left-vs-right transformation)
- RNP composition order (TIRS → ITRS, polar motion sign)
- LVLH and NED frame definitions (z-down vs z-radial-out, hand)
- Gravity-gradient torque sign and frame
- Atmosphere lookup interpolation rule (log-density vs linear, altitude reference)
- Time-scale offsets (TAI − UTC = leap seconds, sign of UT1−UTC)
- Geodetic ↔ geocentric latitude conversion (which iteration, convergence criterion)
What is not a catalog candidate: arithmetic plumbing (a + b), unit conversions handled by the typed-quantity facade, library calls into anise/glam that don't encode a JEOD-specific choice.
Bootstrap path
- Land
docs/JEOD_equations.md with section headings and ~5 seed rows drawn from the canonical near-miss (KE.* for anomaly conversions, RO.* for the quaternion convention covered in CLAUDE.md's "Quaternion Convention" section).
- Tag the existing implementation sites for those seeds.
- Land
tests/equation_coverage.rs with both directions enforced. CI now refuses untagged catalog rows and orphan tags.
- Backfill incrementally: when porting or reviewing a new formula, the author adds a row + tag. No big-bang sweep — the catalog grows when the cost of guessing would be high.
- Once stable, update CLAUDE.md's JEOD Convention Rule section to point at the catalog as the enforcement mechanism, and the JEOD Invariant Tracking section to note the equation-catalog sibling.
Acceptance
docs/JEOD_equations.md exists with seed rows for at least the time_periapsis formula and the quaternion convention.
tests/equation_coverage.rs passes locally and in CI, enforcing bidirectional consistency.
CLAUDE.md's JEOD Convention Rule section references the catalog.
- At least one PR after merge demonstrates the workflow end-to-end: a new ported formula lands with a new catalog row and a new
// JEOD_EQ: tag together.
Relationship to #517
This issue is the deferred recommendation 5 from #517. Independent of #517's four items — can land before, after, or in parallel. Shares the same underlying motivation (make AI-assisted port correctness structural) but a different mechanism (traceability instead of structural guard).
Motivation
CLAUDE.md's JEOD Convention Rule exists because of a real near-miss: an agent guessed the
time_periapsis→ mean anomaly formula asM = 2π − n·tinstead ofM = n·t, producing 11,668 km of error against NASA flight data. The rule today reads:It's a cultural rule. An AI-assisted PR that ports a formula without reading the
.cccan satisfy fmt + clippy + most tests and still be silently wrong — exactly the failure shape discussed for issue #517. The fix discussed there (a generic "every new formula needs a.cc:linecitation" lint) was deferred because the citation grammar wasn't tractable.This issue proposes a more tractable shape: a physics-equation catalog that mirrors the existing
docs/JEOD_invariants.md↔// JEOD_INV:↔tests/invariant_coverage.rsmechanism, but for formulas instead of guard conditions. The catalog itself is the citation grammar.Proposal
docs/JEOD_equations.md— a parallel catalogOne row per non-trivial formula we port from JEOD. Each row carries:
Section.Item, e.g.KE.03(Kepler/anomaly conversions),RO.07(rotation),GR.12(gravity gradient),AT.02(atmosphere),TM.05(time scales),RF.04(frame transforms).models/.../*.cc:linereference, with the relevant function name. Example:models/dynamics/body_action/src/dyn_body_init_orbit.cc:218 dyn_body_init_orbit::apply().crates/astrodyn_orbital/src/elements.rs:mean_anomaly_from_time_since_periapsis).Source-side tags
Mirror the
JEOD_INVpattern:The tag goes at the formula site, not at the function signature, so derivations that compose two catalog entries can carry both tags inline.
tests/equation_coverage.rs— bidirectional CI gateParallel to
tests/invariant_coverage.rs:JEOD_equations.mdmust have at least one// JEOD_EQ: XX.YYtag in the workspace source.// JEOD_EQ: XX.YYtag must reference a catalog row.models/.../*.cc:<line>ormodels/.../*.hh:<line>); the test does not attempt to verify the citation against an actual JEOD checkout (CI runs without$JEOD_HOME, per CLAUDE.md), but the format check rules out garbage.This is the citation grammar: rows in the catalog are the allowed citations, and tags are the allowed call sites.
Why a catalog and not a free-form citation lint
A "require
.cc:linein any new formula" lint has two failure modes that the catalog avoids:v.normalize()call doesn't need a citation; a quaternion-from-Euler-angles construction does. A regex can't tell them apart; a catalog row makes the boundary explicit ("if it's in the catalog, it needs a tag; if it's not in the catalog, an author who thinks it should be opens a PR adding the row")..cc:linecomment goes stale silently when JEOD reorganizes. A central catalog can be re-grepped/re-verified in one place during a JEOD bump, the same wayextract_*binaries are.This shape also matches what already works:
JEOD_INVsolves an analogous problem and the team is fluent in the pattern.Scope discipline
The catalog must not explode into an enumeration of every line of math. Inclusion criterion: the formula has a convention choice that could be guessed wrong and produce silently-wrong physics that passes shallow tests.
Candidates on day one (non-exhaustive — actual numbering chosen during implementation):
time_periapsis)What is not a catalog candidate: arithmetic plumbing (
a + b), unit conversions handled by the typed-quantity facade, library calls intoanise/glamthat don't encode a JEOD-specific choice.Bootstrap path
docs/JEOD_equations.mdwith section headings and ~5 seed rows drawn from the canonical near-miss (KE.*for anomaly conversions,RO.*for the quaternion convention covered in CLAUDE.md's "Quaternion Convention" section).tests/equation_coverage.rswith both directions enforced. CI now refuses untagged catalog rows and orphan tags.Acceptance
docs/JEOD_equations.mdexists with seed rows for at least thetime_periapsisformula and the quaternion convention.tests/equation_coverage.rspasses locally and in CI, enforcing bidirectional consistency.CLAUDE.md's JEOD Convention Rule section references the catalog.// JEOD_EQ:tag together.Relationship to #517
This issue is the deferred recommendation 5 from #517. Independent of #517's four items — can land before, after, or in parallel. Shares the same underlying motivation (make AI-assisted port correctness structural) but a different mechanism (traceability instead of structural guard).