@essrt/physics 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/model.js +393 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essrt/physics",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "ESSRT (Expanding Solar System Resonance Theory) physics core \u2014 pure, injectable-constants, no I/O, no globals, no DOM. The complete model incl. fitted coefficients; every published version immutably ships one recorded model identity.",
5
5
  "essrt": {
6
6
  "modelVersion": "v10.0",
package/src/model.js CHANGED
@@ -18,11 +18,20 @@
18
18
  * which itself mirrors tools/lib/constants.js §9.
19
19
  */
20
20
  import { deriveEpochParams } from './layer0/derive-params.js';
21
+ import * as FL from './planets/fibonacci-laws.cjs';
22
+ import * as planetOrientation from './planets/orientation.cjs';
21
23
  import { createPhaseMachinery } from './phase/index.cjs';
22
24
  import { createCardinalModel } from './cardinal/index.cjs';
23
25
  import { createDeltaTCycles } from './deltat/cycles.cjs';
24
26
  import { createDeepTimeLod } from './deltat/deep-time.cjs';
25
27
  import { evalClimateL1OrbitalPermil } from './climate/l1-orbital.cjs';
28
+ import { createMoonEccChannel } from './moon/ecc-channel.cjs';
29
+ import { createMoonMonthChain } from './moon/month-chain.cjs';
30
+ import { createChainCycleIntegrator } from './chain-cycles/index.cjs';
31
+ import { createMoonArguments, jdToDecimalYear } from './moon/arguments.cjs';
32
+ import { createMoonSeries } from './moon/series.cjs';
33
+ import { createEclipseFinders } from './eclipse/finders.cjs';
34
+ import { driver2PeriodSecondsAtAge } from './planets/orbit-chain.cjs';
26
35
 
27
36
  /**
28
37
  * RA-day-offset Fourier amplitudes (ms). KNOWN EXCEPTION carried over from
@@ -34,6 +43,17 @@ const RA_DAY_OFFSET_MEAN_MS = -14.194;
34
43
  const RA_DAY_OFFSET_ECC_MS = -5.64;
35
44
  const RA_DAY_OFFSET_OBLIQ_MS = -1.684;
36
45
 
46
+ /**
47
+ * Moon-channel eccentricity sensitivities (perigee/node), the [g/g₀]^s
48
+ * exponents of the factored deep-time law. Same known-exception class as the
49
+ * RA offsets above: single source src/script.js _FW_MOON, mirrored as
50
+ * literals in tools/lib/deep-time.js (_ECOMP_S_W/_ECOMP_S_N). Both are
51
+ * Meeus-effective — S_N moved 1.0 → 1.018 with the v4 frame-attribution
52
+ * batch.
53
+ */
54
+ const MOON_ECC_SENSITIVITY_PERIGEE = 2.407;
55
+ const MOON_ECC_SENSITIVITY_NODE = 1.018;
56
+
37
57
  /**
38
58
  * Assemble the model surfaces from a resolved constants context + fitted
39
59
  * coefficients. Internal: `createModel` in index.js composes this with the
@@ -399,6 +419,128 @@ export function assembleModel(C, F) {
399
419
  /** @param {number} year @returns {number} */
400
420
  const deltaTSeconds = (year) => C.earthOrbital.deltaTStart + meanDeltaTSecondsAtAge(yearToTMa(year));
401
421
 
422
+ // ── Planets: Fibonacci-law derivation chain + orientation ─────────────────
423
+ const PLANET_KEYS = ['mercury', 'venus', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune'];
424
+ const massEarthAlone = GM_EARTH_ALONE / G_CONSTANT;
425
+ /** @type {Record<string, number>} */
426
+ const massFraction = {};
427
+ for (const k of PLANET_KEYS) massFraction[k] = 1 / C.physicalConstants.massRatioDE440[k];
428
+ massFraction.earth = massEarthAlone / M_SUN;
429
+
430
+ const PSI = FL.computePsiConstant({
431
+ earthInvPlaneInclinationAmplitude: earthInclAmplitude,
432
+ massEarthAlone,
433
+ massSun: M_SUN,
434
+ });
435
+ const eccentricityAmplitudeK = FL.computeKConstant({
436
+ eccentricityAmplitude,
437
+ massEarthAlone,
438
+ massSun: M_SUN,
439
+ earthTiltMeanDeg: earthtiltMean,
440
+ });
441
+ const systemResetN = C.foundational.systemResetN;
442
+ const t2000 = 2000 - (balancedYear - systemResetN * H);
443
+ const balancedJD = startmodelJD - meanSolarYearDays * (startModelYearWithCorrection - balancedYear);
444
+
445
+ /** @param {[number, number]|null} frac @returns {number|null} */
446
+ const fractionToYears = (frac) => (frac === null ? null : (H * frac[0]) / frac[1]);
447
+
448
+ /** @type {Record<string, Record<string, any>>} */
449
+ const PLANET_RECORDS = {};
450
+ for (const k of PLANET_KEYS) {
451
+ const mp = C.planets[k];
452
+ const ar = C.planetOrbitalElements[k];
453
+ const ecl = /** @type {number} */ (fractionToYears(mp.perihelionEclipticFraction));
454
+ const axial = /** @type {number} */ (fractionToYears(mp.axialPrecessionFraction));
455
+ const obliquityCycle = fractionToYears(mp.obliquityCycleFraction)
456
+ ?? Math.abs(1 / (1 / ecl - 1 / (H / 13)));
457
+ const wobble = FL.computeWobblePeriodYears(ecl, axial, H);
458
+ const il = FL.computeInclinationLaw({
459
+ fibonacciD: mp.fibonacciD,
460
+ massFrac: massFraction[k],
461
+ invPlaneInclinationJ2000: ar.invPlaneInclinationJ2000,
462
+ longitudePerihelion: ar.longitudePerihelion,
463
+ inclinationCycleAnchor: mp.inclinationCycleAnchor,
464
+ antiPhase: mp.antiPhase || false,
465
+ }, PSI);
466
+ const obliquityMean = FL.computeObliquityMeanSnapshot({
467
+ axialTiltJ2000: ar.axialTiltJ2000,
468
+ invPlaneInclinationAmplitude: il.amplitude,
469
+ perihelionEclipticYears: ecl,
470
+ }, obliquityCycle, { H, t2000 });
471
+ const el = FL.computeEccentricityLaw({
472
+ fibonacciD: mp.fibonacciD,
473
+ massFrac: massFraction[k],
474
+ solarYearInput: ar.solarYearInput,
475
+ orbitalEccentricityJ2000: ar.orbitalEccentricityJ2000,
476
+ antiPhase: mp.antiPhase || false,
477
+ }, {
478
+ kConstant: eccentricityAmplitudeK,
479
+ obliquityMeanDeg: obliquityMean,
480
+ wobblePeriodYears: wobble,
481
+ t2000,
482
+ meanSolarYearDays,
483
+ });
484
+ PLANET_RECORDS[k] = Object.freeze({
485
+ name: mp.name,
486
+ perihelionEclipticYears: ecl,
487
+ longitudePerihelion: ar.longitudePerihelion,
488
+ ascendingNodeCyclesIn8H: mp.ascendingNodeCyclesIn8H,
489
+ ascendingNodePeriod: -(8 * H) / mp.ascendingNodeCyclesIn8H,
490
+ axialPrecessionYears: axial,
491
+ obliquityCycle,
492
+ wobblePeriod: wobble,
493
+ fibonacciD: mp.fibonacciD,
494
+ antiPhase: mp.antiPhase || false,
495
+ ascendingNodeInvPlane: mp.ascendingNodeInvPlane,
496
+ inclinationCycleAnchor: mp.inclinationCycleAnchor,
497
+ invPlaneInclinationJ2000: ar.invPlaneInclinationJ2000,
498
+ invPlaneInclinationAmplitude: il.amplitude,
499
+ invPlaneInclinationMean: il.mean,
500
+ obliquityMean,
501
+ orbitalEccentricityJ2000: ar.orbitalEccentricityJ2000,
502
+ orbitalEccentricityAmplitude: el.amplitude,
503
+ orbitalEccentricityBase: el.base,
504
+ eccentricityPhaseJ2000: el.phaseJ2000,
505
+ solarYearInput: ar.solarYearInput,
506
+ axialTiltJ2000: ar.axialTiltJ2000,
507
+ });
508
+ }
509
+
510
+ /** Perihelion longitude (linear lattice rate). @param {string} k @param {number} year @returns {number} */
511
+ const planetPerihelionDeg = (k, year) => {
512
+ const p = PLANET_RECORDS[k];
513
+ return (((p.longitudePerihelion + (360.0 * (year - 2000)) / p.perihelionEclipticYears) % 360) + 360) % 360;
514
+ };
515
+ /** Ascending node on the invariable plane. @param {string} k @param {number} year @returns {number} */
516
+ const planetAscNodeDeg = (k, year) => {
517
+ const p = PLANET_RECORDS[k];
518
+ return planetOrientation.ascendingNodeInvPlaneLinearAt({
519
+ ascendingNodeInvPlane: p.ascendingNodeInvPlane,
520
+ ascendingNodePeriod: p.ascendingNodePeriod,
521
+ perihelionEclipticYears: p.perihelionEclipticYears,
522
+ }, year);
523
+ };
524
+ /** Invariable-plane inclination (signed ICRF rate, scene year→JD axis). @param {string} k @param {number} year @returns {number} */
525
+ const planetInclinationDeg = (k, year) => {
526
+ const p = PLANET_RECORDS[k];
527
+ const jd = startmodelJD + (year - startmodelYear) * meanSolarYearDays;
528
+ const yearsSinceBalanced = (jd - balancedJD) / meanSolarYearDays;
529
+ return planetOrientation.invPlaneInclinationAt({
530
+ isEarth: false,
531
+ invPlaneInclinationJ2000: p.invPlaneInclinationJ2000,
532
+ invPlaneInclinationMean: p.invPlaneInclinationMean,
533
+ invPlaneInclinationAmplitude: p.invPlaneInclinationAmplitude,
534
+ inclinationCycleAnchor: p.inclinationCycleAnchor,
535
+ longitudePerihelion: p.longitudePerihelion,
536
+ perihelionEclipticYears: p.perihelionEclipticYears,
537
+ antiPhase: p.antiPhase,
538
+ }, yearsSinceBalanced, {
539
+ H,
540
+ yearsFromBalancedToJ2000: (startmodelJD - balancedJD) / meanSolarYearDays,
541
+ });
542
+ };
543
+
402
544
  // ── Time axis: exact JD ↔ model-year conversion ───────────────────────────
403
545
  // The model's `year` inputs live on the SI axis (the axis the fits were
404
546
  // anchored on — tools/lib `_jdToSIyear`): linear in SI 86400-s days from the
@@ -412,6 +554,235 @@ export function assembleModel(C, F) {
412
554
  /** JD(TT) at a model year (SI axis). @param {number} year @returns {number} */
413
555
  const jdFromYear = (year) => startmodelJD + (year - startModelYearWithCorrection) * siTropicalYearDays;
414
556
 
557
+ // ── Lunar theory: the shared-chain assembly (§7a slice-2b) ────────────────
558
+ // Wiring mirrors the engine call sites exactly — tools/lib/deep-time.js
559
+ // (ecc channel, month chain, J2000 precession anchors),
560
+ // tools/lib/scene-graph.js (chain-cycles, arguments, series) and
561
+ // tools/verify/eclipse-audit.js (finders). Deep-time and framework-native
562
+ // are hardwired ON here: they are the shipped defaults; the A/B env
563
+ // toggles (SG_DEEP_TIME/MOON_ARGS_PURE_MEEUS) stay an engine concern.
564
+ const j2000JD = 2451545.0;
565
+ const julianCenturyDays = 36525;
566
+
567
+ // J2000 Moon precession anchors (Option C+ — of-date observational anchors
568
+ // in the legacy-'ICRF'-named inputs; the E values are star-referenced ∓13)
569
+ const nApsidalIJ2000 = Math.round((8 * totalDaysInH) / C.moonReference.moonApsidalPrecessionDaysInputICRF) / 8;
570
+ const nNodalIJ2000 = Math.round((8 * totalDaysInH) / C.moonReference.moonNodalPrecessionDaysInputICRF) / 8;
571
+ const nApsidalEJ2000 = nApsidalIJ2000 - 13;
572
+ const nNodalEJ2000 = nNodalIJ2000 + 13;
573
+ const moonApsidalJ2000Seconds = (totalDaysInH / nApsidalEJ2000) * meanLengthOfDay;
574
+ const moonNodalJ2000Seconds = (totalDaysInH / nNodalEJ2000) * meanLengthOfDay;
575
+ const moonSiderealMonthJ2000Seconds = moonSiderealMonth * meanLengthOfDay;
576
+
577
+ // 8H-lattice derived months (constants.js §Moon derived months)
578
+ const nSid = Math.round((8 * totalDaysInH) / moonSiderealMonthInput) / 8;
579
+ const moonTropicalMonthDays = totalDaysInH / (nSid + 13);
580
+ const moonAnomalisticMonthDays = totalDaysInH / (nSid - nApsidalEJ2000);
581
+ const moonSynodicMonthDays = totalDaysInH / (nSid + 13 - H);
582
+
583
+ // The framework H/3 eccentricity line (the Moon channel's view of the
584
+ // wobble movement — NOT the H/16 orbital eccentricity above)
585
+ const moonEcc = createMoonEccChannel({
586
+ cyclesBetween,
587
+ eccentricityBase,
588
+ perihelionLongitudeJ2000Deg: C.earthOrbital.earthPerihelionLongitudeJ2000,
589
+ inclinationCycleAnchorDeg: C.earthOrbital.earthInclinationCycleAnchor,
590
+ });
591
+
592
+ // Layer-2 month/precession chain (Brouwer-Clemence m² scaling × the
593
+ // e_E-line modulation)
594
+ const moonChain = createMoonMonthChain({
595
+ constants: {
596
+ aMoonNowMetres: EPOCH_PARAMS.moonDistanceNowM,
597
+ alpha1PerMa: EPOCH_PARAMS.alpha1PerMa,
598
+ alpha3PerMa3: EPOCH_PARAMS.alpha3PerMa3,
599
+ alpha4PerMa4: EPOCH_PARAMS.alpha4PerMa4,
600
+ gmEarthMoonM3PerS2: EPOCH_PARAMS.gmEarthMoonM3S2,
601
+ massRatioEarthMoon: MASS_RATIO_EARTH_MOON,
602
+ moonSiderealMonthInputDays: moonSiderealMonthInput,
603
+ holisticYearJ2000: H,
604
+ meanSiderealYearJ2000Seconds: meanSiderealYearSeconds,
605
+ nApsidalOfDateJ2000: nApsidalIJ2000,
606
+ nNodalOfDateJ2000: nNodalIJ2000,
607
+ moonApsidalJ2000Seconds,
608
+ moonNodalJ2000Seconds,
609
+ moonSiderealMonthJ2000Seconds,
610
+ sPerigee: MOON_ECC_SENSITIVITY_PERIGEE,
611
+ sNode: MOON_ECC_SENSITIVITY_NODE,
612
+ },
613
+ fns: {
614
+ meanLodSecondsAtAge: /** @param {number} tMa */ (tMa) => deepLod.lodSecondsAtAge(tMa),
615
+ meanSiderealYearSecondsAtAge: /** @param {number} tMa */ (tMa) => deepLod.siderealYearSecondsAtAge(tMa),
616
+ meanHAtAge: /** @param {number} tMa */ (tMa) => deepLod.hAtAge(tMa),
617
+ modulation: /** @param {number} tMa @param {number} s */ (tMa, s) => moonEcc.modulation(tMa, s),
618
+ },
619
+ });
620
+
621
+ // Chain-cycle integrator. S5/S12 conventions: age anchor = startmodelYear
622
+ // (the scene's t_Ma convention), grid anchor C(2000) = 0 — grid anchor ≠
623
+ // age anchor, deliberately. One stable period fn per chain so the shared
624
+ // Float64Array tables key correctly and persist.
625
+ const chainCycles = createChainCycleIntegrator({
626
+ ageAnchorYear: startmodelYear,
627
+ tropicalYearSecondsAtAge: /** @param {number} tMa */ (tMa) => deepLod.tropicalYearSecondsAtAge(tMa),
628
+ tropicalYearJ2000Seconds: meanTropicalYearJ2000Seconds,
629
+ isDeepTime: () => true,
630
+ });
631
+ /** @param {number} tMa @returns {number|null} */
632
+ const nodalMonthPeriodFn = (tMa) => moonChain.nodalMonthSecondsAtAge(tMa);
633
+ /** @param {number} tMa @returns {number|null} */
634
+ const tropicalMonthPeriodFn = (tMa) => moonChain.tropicalMonthSecondsAtAge(tMa);
635
+ /** @param {number} tMa @returns {number|null} */
636
+ const anomalisticMonthPeriodFn = (tMa) => moonChain.anomalisticMonthSecondsAtAge(tMa);
637
+ const jupiterT0Seconds = C.planetOrbitalElements.jupiter.solarYearInput * 86400;
638
+ /** @param {number} tMa @returns {number} */
639
+ const jupiterPeriodFn = (tMa) => driver2PeriodSecondsAtAge(tMa, jupiterT0Seconds, EPOCH_PARAMS.solarMassLossFracPerYear);
640
+ /** @param {number} a @param {number} b @returns {number|null} */
641
+ const mcDraconic = (a, b) => chainCycles.cyclesBetween(nodalMonthPeriodFn, a, b);
642
+ /** @param {number} a @param {number} b @returns {number|null} */
643
+ const mcTropical = (a, b) => chainCycles.cyclesBetween(tropicalMonthPeriodFn, a, b);
644
+ /** @param {number} a @param {number} b @returns {number|null} */
645
+ const mcAnomalistic = (a, b) => chainCycles.cyclesBetween(anomalisticMonthPeriodFn, a, b);
646
+ /** @param {number} a @param {number} b @returns {number|null} */
647
+ const mcJupiter = (a, b) => chainCycles.cyclesBetween(jupiterPeriodFn, a, b);
648
+ /** @param {number} a @param {number} b @returns {number|null} */
649
+ const mcApsidalOfDate = (a, b) => {
650
+ const t = mcTropical(a, b), n = mcAnomalistic(a, b);
651
+ return (t === null || n === null) ? null : t - n;
652
+ };
653
+ /** @param {number} a @param {number} b @returns {number|null} */
654
+ const mcNodalOfDate = (a, b) => {
655
+ const dr = mcDraconic(a, b), t = mcTropical(a, b);
656
+ return (dr === null || t === null) ? null : dr - t;
657
+ };
658
+
659
+ // Snapshot-phase obliquity — the engine's computeObliquityEarth convention
660
+ // for the lunar chain (linear H-lattice phase; orbital-engine.js). NOT the
661
+ // integrated-phase display obliquity above — the chain was certified
662
+ // against this form.
663
+ /** @param {number} year @returns {number} */
664
+ const obliquitySnapshotDeg = (year) => {
665
+ const t = year - balancedYear;
666
+ let obliq = solsticeObliquityMean;
667
+ for (const [div, sinC, cosC] of F.SOLSTICE_OBLIQUITY_HARMONICS) {
668
+ const ph = (2 * Math.PI * t) / (H / div);
669
+ obliq += sinC * Math.sin(ph) + cosC * Math.cos(ph);
670
+ }
671
+ return obliq;
672
+ };
673
+
674
+ // The argument skeleton (the _FW_MOON bundle; Sun secular deviations on
675
+ // the CALENDAR year coordinate — S3)
676
+ const moonArgs = createMoonArguments({
677
+ constants: {
678
+ j2000JD,
679
+ julianCenturyDays,
680
+ holisticYearJ2000: H,
681
+ balancedYearJ2000: balancedYear,
682
+ meanSolarYearDays,
683
+ meanAnomalisticYearDays,
684
+ tropicalYearHarmonics: F.TROPICAL_YEAR_HARMONICS,
685
+ anomalisticYearHarmonics: F.ANOMALISTIC_YEAR_HARMONICS,
686
+ eccentricityJ2000: C.earthOrbital.earthEccentricityJ2000,
687
+ eccentricityDotJ2000: C.earthOrbital.earthEccentricityDotJ2000,
688
+ eccentricityDotDotJ2000: C.earthOrbital.earthEccentricityDotDotJ2000,
689
+ elpEarthFigureJ2ArcsecPerCy2: C.moonMeeus.elpW1T2Decomposition_arcsecPerCy2.earthFigureJ2,
690
+ elpGeneralPrecessionPA_T2ArcsecPerCy2: C.moonMeeus.elpW1T2Decomposition_arcsecPerCy2.generalPrecessionPA_T2_Lieske1976,
691
+ eccE0: moonEcc.e0,
692
+ },
693
+ fns: {
694
+ eccAt: /** @param {number} tYr */ (tYr) => moonEcc.eccAt(tYr),
695
+ channelIntegral: /** @param {number} T @param {number} s */ (T, s) => moonEcc.channelIntegral(T, s),
696
+ computeObliquityEarth: obliquitySnapshotDeg,
697
+ jdToSIyear: yearFromJD,
698
+ tropicalOrbitsBetween: mcTropical,
699
+ apsidalOfDateCyclesBetween: mcApsidalOfDate,
700
+ nodalOfDateCyclesBetween: mcNodalOfDate,
701
+ cyclesBetween,
702
+ isDeepTime: () => true,
703
+ isFrameworkNative: () => true,
704
+ },
705
+ });
706
+
707
+ // UT→TT on the CALENDAR decimal-year coordinate (script.js Phase 9.16 —
708
+ // a linear-year approximation here once cost ~5–6 s of ΔT and ~1e-3° of
709
+ // Moon longitude at the Babylonian epochs)
710
+ /** @param {number} jd @returns {number} */
711
+ const jdTTFromUT = (jd) => {
712
+ const tMa = (startmodelYear - jdToDecimalYear(jd)) / 1e6;
713
+ const dT = meanDeltaTSecondsAtAge(tMa);
714
+ return Number.isFinite(dT) ? jd + dT / 86400 : jd;
715
+ };
716
+ // ΔT at a JD on the CALENDAR-year axis (mirrors deep-time frameworkDeltaT;
717
+ // NOT deltaTSeconds above, which adds the deltaTStart anchor)
718
+ /** @param {number} jd @returns {number} */
719
+ const frameworkDeltaTSecondsAtJD = (jd) => {
720
+ const decYear = startmodelYear + (jd - startmodelJD) / meanSolarYearDays;
721
+ const dT = meanDeltaTSecondsAtAge((startmodelYear - decYear) / 1e6);
722
+ return Number.isFinite(dT) ? dT : 0;
723
+ };
724
+
725
+ // Bounded Meeus E-factor from the H/3 line (framework-native branch only;
726
+ // the pure-Meeus polynomial A/B branch stays engine-local)
727
+ /** @param {number} dDays @returns {number} */
728
+ const fwEFactor = (dDays) => moonEcc.eFactorAt(dDays / C.foundational.inputmeanlengthsolaryearindays);
729
+
730
+ // D2 derived additional-argument rates (deg/cy, J2000 8H-lattice months;
731
+ // record: tools/explore/derive-a1a2a3.js)
732
+ const fwA2RateDegPerCy = 2 * ((360 * 36525) / moonTropicalMonthDays)
733
+ - (360 * 36525) / moonAnomalisticMonthDays
734
+ - 2 * ((360 * 36525) / C.planetOrbitalElements.jupiter.solarYearInput);
735
+ const fwA3RateDegPerCy = (360 * 36525) / moonSiderealMonth;
736
+
737
+ // Meeus Ch. 47 truncated series (framework-native arguments + E-factor)
738
+ const moonSeries = createMoonSeries({
739
+ constants: {
740
+ moonL: F.MEEUS_LONGITUDE_TERMS,
741
+ moonB: F.MEEUS_LATITUDE_TERMS,
742
+ j2000JD,
743
+ julianCenturyDays,
744
+ moonMeeusLpCorrectionDeg: C.moon.moonMeeusLpCorrection,
745
+ fwA2RateDegPerCy,
746
+ fwA3RateDegPerCy,
747
+ },
748
+ fns: {
749
+ argsAt: /** @param {number} jdTT */ (jdTT) => moonArgs.argsAt(jdTT),
750
+ eFactorForD: fwEFactor,
751
+ eFactorAtJdTT: /** @param {number} jdTT */ (jdTT) => fwEFactor(jdTT - j2000JD),
752
+ getMoonDistanceKm: () => moonDistanceKm,
753
+ getEccentricityBase: () => C.moonReference.moonOrbitalEccentricityBase,
754
+ deltaTSeconds: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
755
+ jdToSIyear: yearFromJD,
756
+ tropicalOrbitsBetween: mcTropical,
757
+ apsidalOfDateCyclesBetween: mcApsidalOfDate,
758
+ cyclesBetween,
759
+ jupiterOrbitsBetween: mcJupiter,
760
+ isDeepTime: () => true,
761
+ isFrameworkNative: () => true,
762
+ },
763
+ });
764
+
765
+ // Eclipse finders — wired like the engine probe (tools/verify/
766
+ // eclipse-audit.js). The finder axis is JD(UT): the series wrapper applies
767
+ // UT→TT internally. Ground-track/umbra paths deliberately absent: the
768
+ // scene-umbra projection navigates the Tychosium-derived scaffold, which
769
+ // never enters this package (§2h).
770
+ const eclipseFinders = createEclipseFinders({
771
+ moonLonDegAt: /** @param {number} jd */ (jd) => moonSeries.truncatedLonDeg(jd),
772
+ moonBetaDegAt: /** @param {number} jd */ (jd) => moonSeries.truncatedBetaDeg(jd),
773
+ moonDistanceKmAt: /** @param {number} jd */ (jd) => moonSeries.truncatedDistanceKm(jd),
774
+ deltaTSecondsAt: frameworkDeltaTSecondsAtJD,
775
+ getSynodicMonthDays: () => moonSynodicMonthDays,
776
+ getSunDistanceKm: () => currentAUDistance,
777
+ constants: {
778
+ rEarthMetres: (C.bodyDiametersKm.earth / 2) * 1000,
779
+ moonDiameterKm: C.bodyDiametersKm.moon,
780
+ sunDiameterKm: C.bodyDiametersKm.sun,
781
+ j2000JD,
782
+ julianCenturyDays,
783
+ },
784
+ });
785
+
415
786
  // ── The assembled surface ─────────────────────────────────────────────────
416
787
  return Object.freeze({
417
788
  time: Object.freeze({
@@ -458,6 +829,28 @@ export function assembleModel(C, F) {
458
829
  moon: Object.freeze({
459
830
  distanceKmAtYear: /** @param {number} year @returns {number} */ (year) => moonDistanceMetresAtAge(yearToTMa(year)) / 1000,
460
831
  siderealMonthDaysAtYear: moonSiderealMonthDaysAt,
832
+ synodicMonthDays: moonSynodicMonthDays,
833
+ // The apparent-position chain (truncated Meeus Ch. 47 series on
834
+ // framework-native arguments). JD(UT) axis — UT→TT applied internally.
835
+ lonDegAtJD: /** @param {number} jd @returns {number} */ (jd) => moonSeries.truncatedLonDeg(jd),
836
+ betaDegAtJD: /** @param {number} jd @returns {number} */ (jd) => moonSeries.truncatedBetaDeg(jd),
837
+ distanceKmAtJD: /** @param {number} jd @returns {number} */ (jd) => moonSeries.truncatedDistanceKm(jd),
838
+ }),
839
+ eclipse: Object.freeze({
840
+ sunLonDegAtJD: /** @param {number} jd @returns {number} */ (jd) => eclipseFinders.sunLonDegAt(jd),
841
+ findLunarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findLunarEclipsesInRange(jdStart, jdEnd),
842
+ findSolarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findSolarEclipsesInRange(jdStart, jdEnd),
843
+ deltaTSecondsAtJD: frameworkDeltaTSecondsAtJD,
844
+ }),
845
+ climate: Object.freeze({
846
+ l1OrbitalPermil: evalClimateL1,
847
+ }),
848
+ planets: Object.freeze({
849
+ keys: Object.freeze([...PLANET_KEYS]),
850
+ record: /** @param {string} k @returns {Record<string, any>|undefined} */ (k) => PLANET_RECORDS[k],
851
+ perihelionLongitudeDeg: planetPerihelionDeg,
852
+ ascendingNodeInvPlaneDeg: planetAscNodeDeg,
853
+ invPlaneInclinationDeg: planetInclinationDeg,
461
854
  }),
462
855
  });
463
856
  }