YEAR_SEMANTICS_AUDIT - TheDaniel166/moira GitHub Wiki

Year Semantics Audit

Date: 2026-03-30 Corrected: 2026-07-25

Profection correction. The earlier audit classified a fixed elapsed_days / 365.25 age as plausibly correct. That classification was stale. profection_schedule() now advances completed age on the natal civil anniversary in the natal timezone. February 29 nativities require an explicit february_28 or march_1 anniversary policy.

Purpose

Audit every live use of 365.25, 36525.0, JULIAN_YEAR, TROPICAL_YEAR, and SIDEREAL_YEAR in the moira/ codebase so the project distinguishes:

  • exact Julian-year doctrine
  • tropical-year timing
  • sidereal-year/orbital timing
  • standard Julian-date algorithm constants
  • sites that remain candidates for future refinement

Summary Verdict

Most remaining 365.25 uses are correct and should remain.

The codebase currently contains seven distinct semantic classes:

  1. Julian year by doctrine or convention: keep.
  2. Tropical year for seasonal/solar timing: use TROPICAL_YEAR.
  3. Sidereal year for orbital/stellar period timing: use SIDEREAL_YEAR.
  4. Julian century (36525.0) in standard astronomical polynomials: keep.
  5. 365.25 inside canonical calendar/JD conversion algorithms: keep.
  6. Delta-T annual-epoch integration: use actual source-epoch spacing.
  7. Civil-anniversary chronology for completed ages: do not replace with a fixed-length astronomical year.

After the latest correction pass, the main live upgrade targets that remain are small in number. The largest genuinely ambiguous site is the annual-resolution integration in delta_t_physical.py.

Class A: Keep As Exact Julian-Year Doctrine

These are not loose approximations. They are explicit Julian-year doctrine, historical astrological convention, or standard astronomical convention.

  • moira/constants.py
    • JULIAN_YEAR = 365.25
    • Correct. Exact by definition.
  • moira/dasha.py
    • julian_365.25 basis and all JULIAN_YEAR uses
    • Correct. This is an explicit doctrinal year basis, contrasted with savana_360.
  • moira/timelords.py
    • _JULIAN_YEAR = 365.25
    • Correct. The module explicitly says JD arithmetic uses Julian years.
  • moira/primary_directions/keys.py
    • _NAIBOD_RATE = 360.0 / 365.25
    • Correct. Naibod is a doctrinal key, not a generic solar-year approximation.
  • moira/primary_directions/__init__.py
    • _DEFAULT_SOLAR_RATE = 360.0 / 365.25
    • Likely correct within primary-direction key doctrine unless the subsystem explicitly adopts a different key basis.
  • moira/stars.py
    • proper-motion propagation: dt_years = (jd_tt - _J2000) / 365.25
    • light-time conversion: _DAYS_PER_YEAR = 365.25
    • Correct by astronomical convention. Proper motion is normally expressed per Julian year; the light-year is conventionally tied to the Julian year.
  • moira/multiple_stars.py
    • OrbitalElements.period_yr is documented as Julian years
    • t_yr = (jd - orb.epoch_jd) / 365.25
    • Correct because the module explicitly defines orbital periods in Julian years.
  • moira/sothic.py
    • 1460 × 365.25 = 533265
    • cycle_pos = ((jd_rise - epoch_jd) / 365.25) % _SOTHIC_CYCLE_YEARS
    • Correct. The Sothic cycle identity is specifically Julian-year based.

Class B: Keep As Tropical-Year Timing

These are places where the code means seasonal or solar-cycle timing rather than a generic Julian year.

  • moira/constants.py
    • TROPICAL_YEAR = 365.24219
  • moira/harmonics.py
    • age harmonic timing uses TROPICAL_YEAR
    • Correct.
  • moira/progressions.py
    • progression age conversion uses TROPICAL_YEAR
    • Correct.
  • moira/transits.py
    • solar_return() search seed now uses TROPICAL_YEAR
    • Correct. The solar return is seasonal/solar, not Julian-year doctrine.

Class C: Keep As Sidereal-Year / Orbital Timing

These are places where the code means orbital period or sidereal cycle timing.

  • moira/constants.py
    • SIDEREAL_YEAR = 365.256363
    • Body.SIDEREAL_PERIODS[EARTH] = SIDEREAL_YEAR
    • Correct.
  • moira/phenomena.py
    • perihelion/aphelion fallback search windows now use SIDEREAL_YEAR
    • Correct. These are orbital search heuristics, not seasonal year lengths.

Class D: Keep As Julian-Century Polynomial Time

These are standard astronomical polynomial scales. They are not “approximate years” in the sense at issue here.

  • moira/constants.py
    • JULIAN_CENTURY = 36525.0
  • moira/coordinates.py
    • T = (jd_tt - 2451545.0) / 36525.0
  • moira/eclipse.py
    • centuries = delta_days / 36525.0
  • moira/hermetic_decans.py
    • T = (jd - 2451545.0) / 36525.0
  • moira/nodes.py
    • rates divided by 36525.0
  • moira/precession.py
    • T * 36525.0

These should remain exactly as they are unless the underlying polynomial model itself changes.

Class E: Keep As Canonical JD/Calendar Algorithm Terms

These uses occur inside standard Meeus-style or related calendar/JD conversion formulae. They are algorithm constants, not physical year approximations.

  • moira/julian.py
    • math.floor(365.25 * (year + 4716))
    • (B - 122.1) / 365.25
    • D = math.floor(365.25 * C)
  • moira/planets.py
    • inverse calendar/JD helper with the same constants

These should not be rewritten as tropical or sidereal years.

Class F: Delta-T Annual-Epoch Integration — Resolved

moira/delta_t_physical.py no longer treats every annual-series interval as an undifferentiated 365.25-day span. Calendar mid-year epochs use their actual Julian-day spacing, including leap-year structure. A JULIAN_YEAR fallback remains only for an explicitly uniform decimal-year axis whose input does not carry calendar mid-year semantics.

The candidate C04, AAM/OAM, and GRACE component series are also quarantined from the admitted public Delta-T mean. Their epoch-spacing helpers remain testable research infrastructure; they do not establish causal component truth merely by integrating a proxy.

Class G: Civil-Anniversary Chronology

  • moira/profections.py
    • profection_schedule() accepts timezone-aware natal_dt and current_dt.
    • Completed age changes at the natal local date and time, after converting the current instant into the natal timezone.
    • A February 29 nativity requires an explicit LeapDayAnniversaryPolicy.FEBRUARY_28 or LeapDayAnniversaryPolicy.MARCH_1.
    • The result carries age_basis="civil_anniversary" and the selected leap-day policy.

This is a calendar rule, not a Julian-, tropical-, or sidereal-year constant. Using int(elapsed_days / 365.25) can advance the profection before the civil anniversary and is forbidden for the schedule API.

Low-Priority Cosmetic Improvements

These are not correctness bugs, but could be made more explicit over time.

  • moira/stars.py
    • could import JULIAN_YEAR instead of local _DAYS_PER_YEAR = 365.25
  • moira/timelords.py
    • could import JULIAN_YEAR instead of local _JULIAN_YEAR = 365.25
  • moira/multiple_stars.py
    • could import JULIAN_YEAR for consistency with its “Julian years” doctrine

These are semantic-clarity refactors, not accuracy fixes.

Recommended Next Steps

  1. Leave all admitted Class A-G semantics intact.
  2. Optionally normalize explicit Julian-year imports in Class A for consistency.
  3. Preserve the explicit source-epoch policy and quarantine status when Delta-T data artifacts are refreshed or re-derived.
  4. If desired, annotate key modules with “Julian year”, “tropical year”, or “sidereal year” comments at the constant declaration points to prevent future drift.