@essrt/physics 1.8.0 → 1.10.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essrt/physics",
3
- "version": "1.8.0",
3
+ "version": "1.10.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": "v11.0",
@@ -36,6 +36,7 @@
36
36
  "./chain-cycles": "./src/chain-cycles/index.cjs",
37
37
  "./moon/arguments": "./src/moon/arguments.cjs",
38
38
  "./moon/series": "./src/moon/series.cjs",
39
+ "./moon/series-extension": "./src/moon/series-extension.cjs",
39
40
  "./moon/apparent": "./src/moon/apparent.cjs",
40
41
  "./planets/geometry": "./src/planets/geometry.cjs",
41
42
  "./planets/fibonacci-laws": "./src/planets/fibonacci-laws.cjs",
@@ -58,6 +58,7 @@
58
58
  * full-series Moon: ecliptic-of-date longitude/latitude (deg) + distance (km), TT axis (days since J2000)
59
59
  * @property {(jdUT: number) => number} sunLonDegAt - geometric mean sun longitude (deg), JD(UT) axis with the finder's internal ΔT
60
60
  * @property {(T: number) => number} sunCompletionDeg - planetary completion (deg) at T centuries TT, SUBTRACTED from the finder sun (see eclipse/sun-planetary-completion.cjs; the finders deliberately stay without it — their fitted anchors and certified canon statistics were produced on the bare form, and elongation-class timing absorbs the omission into the fitted phases)
61
+ * @property {(T: number) => {dLonDeg: number, dLatDeg: number}} moonExtensionAt - derived series-extension tail (deg) at T centuries TT, ADDED to the full-series Moon longitude/latitude (see moon/series-extension.cjs; same finder-scoping rationale as sunCompletionDeg — the finders stay on the bare series)
61
62
  * @property {(jd: number) => number} deltaTSecondsAt - framework ΔT (J2000-zeroed convention)
62
63
  * @property {(year: number) => number} obliquityDegAt - framework obliquity (deg) at calendar year
63
64
  * @property {(year: number) => number} eccentricityAt - framework Earth-orbit eccentricity at calendar year
@@ -126,29 +127,41 @@ function createBesselian(deps) {
126
127
  const dTT = (jb - K.j2000JD) + deps.deltaTSecondsAt(jb) / 86400;
127
128
  const sunLon = deps.sunLonDegAt(jb) - deps.sunCompletionDeg(dTT / K.julianCenturyDays);
128
129
  const moon = deps.moonFullAtDaysTT(dTT);
130
+ const ext = deps.moonExtensionAt(dTT / K.julianCenturyDays);
129
131
  return {
130
132
  S: eclToEq(sunLon, 0, sunDistanceKm(year, sunLon), eps),
131
- M: eclToEq(moon.lonDeg, moon.latDeg, moon.distKm, eps),
133
+ M: eclToEq(moon.lonDeg + ext.dLonDeg, moon.latDeg + ext.dLatDeg, moon.distKm, eps),
132
134
  };
133
135
  }
134
136
 
135
137
  /** Umbra-axis ground intersection at JD(UT): geodetic latitude, east
136
138
  * longitude — or null when the axis misses the ellipsoid.
139
+ * EXACT axis ∩ ellipsoid: scaling z by 1/(1−f) turns the WGS84 quadric
140
+ * into a sphere of radius R_E. The earlier R_E-SPHERE intersection put
141
+ * the hit up to R_E·f·sin²φ ≈ 21 km above the true surface, and the
142
+ * offset projects into the shadow-plane metric as ~height·cos(alt) —
143
+ * a latitude/sun-altitude-correlated phantom error (measured: 2021
144
+ * Antarctica 12.8″ → 6.9″, 2008 Arctic 7.3″ → 1.6″, 2026 Iberia
145
+ * low-sun 4.4″ → 1.2″; NASA path tables are ellipsoid-based). The
146
+ * geodetic latitude is then exact for the on-surface point:
147
+ * tan φ = z / ((1−f)²·ρ).
137
148
  * @param {number} jdUT @returns {{latDeg: number, lonDeg: number} | null} */
138
149
  function umbraGroundAt(jdUT) {
139
150
  const { S, M } = bodiesAt(jdUT);
140
151
  const dv = [M[0] - S[0], M[1] - S[1], M[2] - S[2]];
141
152
  const dl = Math.hypot(dv[0], dv[1], dv[2]);
142
153
  const d = [dv[0] / dl, dv[1] / dl, dv[2] / dl];
143
- const MdotD = M[0] * d[0] + M[1] * d[1] + M[2] * d[2];
144
- const MdotM = M[0] * M[0] + M[1] * M[1] + M[2] * M[2];
145
- const disc = MdotD * MdotD - (MdotM - R_E_KM * R_E_KM);
154
+ const w = 1 / (1 - F);
155
+ const Ms = [M[0], M[1], M[2] * w], ds = [d[0], d[1], d[2] * w];
156
+ const A = ds[0] * ds[0] + ds[1] * ds[1] + ds[2] * ds[2];
157
+ const B = Ms[0] * ds[0] + Ms[1] * ds[1] + Ms[2] * ds[2];
158
+ const Cq = Ms[0] * Ms[0] + Ms[1] * Ms[1] + Ms[2] * Ms[2] - R_E_KM * R_E_KM;
159
+ const disc = B * B - A * Cq;
146
160
  if (disc < 0) return null;
147
- const s = -MdotD - Math.sqrt(disc);
161
+ const s = (-B - Math.sqrt(disc)) / A;
148
162
  const hit = [M[0] + s * d[0], M[1] + s * d[1], M[2] + s * d[2]];
149
- const r = Math.hypot(hit[0], hit[1], hit[2]);
150
- const latGc = Math.asin(Math.max(-1, Math.min(1, hit[2] / r)));
151
- const latDeg = Math.atan(Math.tan(latGc) / ((1 - F) * (1 - F))) / D2R;
163
+ const rho = Math.hypot(hit[0], hit[1]);
164
+ const latDeg = Math.atan2(hit[2], (1 - F) * (1 - F) * rho) / D2R;
152
165
  let lonDeg = Math.atan2(hit[1], hit[0]) / D2R - gmstDeg(jdUT);
153
166
  lonDeg = ((lonDeg + 540) % 360) - 180;
154
167
  return { latDeg, lonDeg };
@@ -35,6 +35,17 @@
35
35
 
36
36
  'use strict';
37
37
 
38
+ /**
39
+ * @typedef {Object} FrameworkSunDeps
40
+ * @property {number} sunMeanLongitudeJ2000Deg - L0 anchor (input anchor, 280.46646)
41
+ * @property {number} tropicalRateDegPerCy - the framework tropical rate (from the year chain)
42
+ * @property {(year: number) => number} eccentricityAt - the framework e(t) law
43
+ * @property {(year: number) => number} perihelionLongitudeDegAt - the framework ϖ(t) law (heliocentric)
44
+ * @property {(year: number) => number} [meanLongitudeDegAt] - optional epoch-dependent
45
+ * mean longitude L(t) (e.g. L0 + ∫rate·dt from the f(Y) tropical-year chain);
46
+ * absent = the linear form L0 + rate·T
47
+ */
48
+
38
49
  /**
39
50
  * @typedef {Object} EclipseFinderDeps
40
51
  * @property {(jd: number) => number} moonLonDegAt - truncated-series ecliptic longitude
@@ -43,6 +54,13 @@
43
54
  * @property {(jd: number) => number} deltaTSecondsAt - the engine's ΔT convention
44
55
  * @property {() => number} getSynodicMonthDays - live (epoch-mutable)
45
56
  * @property {() => number} getSunDistanceKm - live (epoch-mutable)
57
+ * @property {FrameworkSunDeps} [frameworkSun] - E4 (the native-Sun landing,
58
+ * plan §12i item 11): when present, sunLonDegAt uses the FRAMEWORK form —
59
+ * linear tropical rate + Kepler EoC (to e³) on the framework e(t)/ϖ(t)
60
+ * laws, TT clock unchanged — instead of the Meeus Ch. 25 polynomials.
61
+ * Absent = the historical Meeus form (every certified number's current
62
+ * basis). The swap is a conscious matched-pair event: the D2 completion's
63
+ * fitted 2lE residue and the certified statistics re-measure with it.
46
64
  * @property {{ rEarthMetres: number, moonDiameterKm: number,
47
65
  * sunDiameterKm: number, j2000JD: number, julianCenturyDays: number }} constants
48
66
  */
@@ -52,10 +70,27 @@ function createEclipseFinders(deps) {
52
70
  const K = deps.constants;
53
71
 
54
72
  /** Sun's geocentric ecliptic longitude in degrees (0–360) at given JD_UT.
55
- * MEAN GEOMETRIC — see the module header. @param {number} jd @returns {number} */
73
+ * MEAN GEOMETRIC — see the module header. Two forms (E4): the framework
74
+ * form when deps.frameworkSun is injected, else Meeus Ch. 25.
75
+ * @param {number} jd @returns {number} */
56
76
  function sunLonDegAt(jd) {
57
77
  const _d2r = Math.PI / 180;
58
78
  const T = (jd + deps.deltaTSecondsAt(jd) / 86400 - K.j2000JD) / K.julianCenturyDays;
79
+ if (deps.frameworkSun) {
80
+ // E4 framework form: linear tropical rate + Kepler EoC (to e³) on the
81
+ // framework e(t)/ϖ(t) laws. Same TT clock; same L0 input anchor.
82
+ const F = deps.frameworkSun;
83
+ const year = 2000 + T * 100;
84
+ const L = F.meanLongitudeDegAt
85
+ ? F.meanLongitudeDegAt(year)
86
+ : F.sunMeanLongitudeJ2000Deg + F.tropicalRateDegPerCy * T;
87
+ const e = F.eccentricityAt(year);
88
+ const M = (L - (F.perihelionLongitudeDegAt(year) + 180)) * _d2r;
89
+ const C = (2 * e - e * e * e / 4) * Math.sin(M)
90
+ + 1.25 * e * e * Math.sin(2 * M)
91
+ + (13 / 12) * e * e * e * Math.sin(3 * M);
92
+ return ((L + C / _d2r) % 360 + 360) % 360;
93
+ }
59
94
  const L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T;
60
95
  const M = (357.52911 + 35999.05029 * T - 0.0001537 * T * T) * _d2r;
61
96
  const C = (1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(M)
@@ -1,88 +1,201 @@
1
1
  /**
2
- * Sun planetary completion — the 20.3h location-tier term table.
2
+ * Sun planetary completion v2 — the DERIVED table (Stage D2, plan §12i
3
+ * item 10; supersedes the v1 fitted 10-term table).
3
4
  *
4
- * The finder Sun (framework linear rate + Kepler EoC + the fitted
5
- * SUN_HARMONICS) omits the classical planetary perturbations of the
6
- * geocentric Sun (the Newcomb Venus/Jupiter/Mars terms and the
7
- * Earth-around-EMB wobble). Measured against JPL Horizons (ObsEcLon,
8
- * geocentric, leading-nutation-bridged to mean-of-date) on 960
9
- * deterministic all-phase epochs 1970–2049: the omission is 10.0″ RMS
10
- * of Sun-longitude scatter; this table removes the coherent part,
11
- * 10.0″ 2.9″ RMS, with every term era-stable (amplitude and phase
12
- * agree between the 1970–2009 and 2010–2049 half-samples).
5
+ * DERIVATION, not fit: the table is extracted from the framework's OWN
6
+ * physics twin epoch-phased 8-body RK4 integrations (Sun, planets, EMB)
7
+ * built entirely from framework constants, planet phases taken from the
8
+ * engine scene graph, differenced full-vs-base3 to isolate the planetary
9
+ * signal, with the EMB secular-perihelion channel projected out (that
10
+ * channel belongs to the framework's own ϖ(t)/e(t) laws). The 68 terms
11
+ * below are the analytic reading of that derived signal: main synodic
12
+ * tones plus eccentricity-modulation sidebands (main ± mean anomaly of
13
+ * the modulating planet, main ± M_E). Table-vs-signal fidelity 0.64″.
14
+ * JPL Horizons enters only as VALIDATION, never as fit target: the
15
+ * derived signal matches JPL residuals at corr −0.997 / slope −0.999
16
+ * (0.60″ combined), and shipping it takes the all-phase Sun residual
17
+ * 3.48″ → 2.12″, the 179-eclipse syzygy fleet 5.51″ → 3.99″, and the
18
+ * NASA centerline reference set 5.30″ → 3.94″ shadow-plane (jointly
19
+ * with the derived Moon series extension, moon/series-extension.cjs —
20
+ * the pre-registered acceptance was all four gates, never a subset).
21
+ * Instruments: tools/explore/d2-derived-sun.mjs (signal),
22
+ * d2-sun-table-extraction.mjs (table), d2-joint-preview.mjs /
23
+ * d2-widened-centerlines.mjs (validation).
13
24
  *
14
- * THE CONSTANT-ATTRIBUTION RULE (the 20.3h-lite trap, resolved here):
15
- * the table carries NO constant term the all-phase fit attributes the
16
- * constant to the tier's existing anchors, so the terms ship without
17
- * the +6″ syzygy-mean penalty that an eclipse-sample fit produced (at
18
- * new moon the EMB argument D locks near 0 and its cos-component
19
- * becomes a constant; fitting on eclipses alone poisons the mean).
20
- * Verified on the 179-eclipse-syzygy sample: elongation mean stays
21
- * +1.1″ +1.5″ while RMS drops 10.6″ → 5.5″; and on the 15 NASA
22
- * path-table centerline points: shadow-plane mean 6.0″ 3.6″, max
23
- * 10.1″ 5.0″, every event improved.
25
+ * DECLARED INPUTS (the three non-framework residues):
26
+ * (i) planet/Sun mass ratios observed IAU constants (shared with the
27
+ * whole framework; nothing here fits them);
28
+ * (ii) argument polynomials IAU-class instrument literals, phase
29
+ * carriers only; their framework-native derivation is research
30
+ * item 20.3e;
31
+ * (iii) the 2lE term (+1.42″ sin 2lE) DECLARED FITTED, not derived: a
32
+ * finder-annual artifact of the current SUN_HARMONICS chain, to be
33
+ * absorbed at the next Step-0 refit. Everything else in this file
34
+ * is derived. The Earth-around-EMB wobble, fitted −6.64″ in v1, is
35
+ * now DERIVED: amplitude a_M·μ/AU with μ = 1/(1+M_E/M_M), injected
36
+ * by the factory below from live package constants (6.4399″ at the
37
+ * current constants), sign negative in the subtract convention.
24
38
  *
25
- * Arguments are of-date mean longitudes (IAU-class instrument
26
- * polynomials, degrees/Julian-century TT). They are phase carriers for
27
- * the fitted amplitudes the framework-native derivation of these
28
- * term tables is the 20.3e research item. Terms with ambiguous
29
- * identifications (the ~8-yr band, the 414-day Venus/three-planet
30
- * blend, E−M, E−S) are deliberately NOT shipped: each failed the
31
- * half-sample stability test or has no unique two-planet argument at
32
- * the window's frequency resolution (~1.5″ RSS accepted as floor).
39
+ * THE CONSTANT-ATTRIBUTION RULE (v1's 20.3h-lite trap, still honored):
40
+ * the table carries NO constant term the constant is attributed to the
41
+ * tier's existing anchors, so the terms ship without the +6″ syzygy-mean
42
+ * penalty an eclipse-sample fit produced.
33
43
  *
34
- * Sign convention: the table models (framework truth), so consumers
35
- * SUBTRACT the evaluation from the finder Sun longitude.
44
+ * Sign convention: unchanged from v1 the evaluation models
45
+ * (framework − truth), so consumers SUBTRACT it from the finder Sun
46
+ * longitude. TERMS literals are stored in the extraction's native sign
47
+ * (d2-sun-table.local.json) and negated in the evaluator, exactly as the
48
+ * validated joint-preview instrument composes them.
49
+ *
50
+ * MATCHED PAIR: the amplitudes are the residual of the CURRENT finder
51
+ * Sun chain (framework rate + EoC + SUN_HARMONICS) and must be
52
+ * RE-DERIVED whenever that chain moves — a Step-0 SUN_HARMONICS refit,
53
+ * an eccentricity/perihelion definition change, or 20.3e. Re-derivation
54
+ * is the D2 instrument chain above (regenerate d2-sun-table.local.json,
55
+ * re-embed). ENFORCED: PAIRED_SUN_HARMONICS_SHA256 below is the
56
+ * fingerprint of the SUN_LONGITUDE_HARMONICS this table was derived
57
+ * under; the create-model parity gate (test:model, in `npm run check`)
58
+ * recomputes it from live constants and fails on mismatch. The api
59
+ * centerline gate (≤8″ shadow-plane) backstops gross staleness
60
+ * independently.
36
61
  */
37
62
 
38
63
  'use strict';
39
64
 
40
- /** Of-date mean-longitude polynomials, degrees per Julian century TT. */
41
- const ARG_RATES = {
42
- lV: [181.979801, 58517.815676], // Venus
43
- lE: [100.466457, 36000.769780], // Earth (EMB)
44
- lM: [355.433000, 19141.696300], // Mars
45
- lJ: [34.351519, 3036.302389], // Jupiter
46
- D: [297.8501921, 445267.1114034], // Moon mean elongation (EMB wobble carrier)
47
- };
65
+ /** Of-date mean-longitude polynomials, degrees per Julian century TT
66
+ * (IAU-class instrument literals — phase carriers, see header (ii)).
67
+ * Body order everywhere below: Mercury, Venus, Earth (EMB), Mars,
68
+ * Jupiter, Saturn. */
69
+ const ARG_L0 = [252.250906, 181.979801, 100.466457, 355.433000, 34.351519, 50.077444];
70
+ const ARG_L1 = [149472.674, 58517.815676, 36000.769780, 19141.696300, 3036.302389, 1223.511013];
71
+ /** J2000 perihelion longitudes (deg), same body order mean anomaly
72
+ * M_X = l_X − ϖ_X; slow ϖ drift is absorbed by the sidebands over the
73
+ * valid window. */
74
+ const PERI = [77.456, 131.564, 102.937, 336.060, 14.331, 93.057];
75
+ /** Moon mean elongation (deg/cy TT) — the EMB-wobble carrier. */
76
+ const ARG_D = [297.8501921, 445267.1114034];
48
77
 
49
78
  /**
50
- * The fitted table: [k·lV, k·lE, k·lM, k·lJ, k·D, cosAmp″, sinAmp″].
51
- * Fit: 960-epoch all-phase LSQ vs JPL Horizons DE441-class ObsEcLon,
52
- * free constant excluded from shipment; SE 0.13 per coefficient.
79
+ * The derived table, extraction-native sign (negated in the evaluator):
80
+ * [[6 mean-longitude multipliers lMe,lV,lE,lM,lJ,lS],
81
+ * [6 mean-anomaly multipliers MMe,MV,ME,MMa,MJ,MS], cos″, sin].
82
+ * 68 terms ≥ 0.05″ from the D2 extraction (79-term LSQ, fidelity 0.64″).
83
+ * @type {Array<[number[], number[], number, number]>}
53
84
  */
54
85
  const TERMS = [
55
- [1, -1, 0, 0, 0, 0, -5.08], // V−E (Venus synodic)
56
- [2, -2, 0, 0, 0, 0, 5.55], // 2(V−E)
57
- [3, -3, 0, 0, 0, 0, 0.68], // 3(V−E)
58
- [2, -3, 0, 0, 0, -2.64, 0], // 2V−3E (the classic Newcomb Venus term)
59
- [0, 1, 0, -1, 0, 0, 7.19], // E−J (Jupiter synodic)
60
- [0, 2, 0, -2, 0, 0, -2.81], // 2(E−J)
61
- [0, 1, 0, -2, 0, -1.34, 0.97],// E−2J
62
- [0, 2, -2, 0, 0, 0, 2.05], // 2(E−M) (Mars)
63
- [0, 2, 0, 0, 0, 0, 1.42], // 2lE (semiannual Kepler leftover)
64
- [0, 0, 0, 0, 1, 0, -6.64], // D (Earth-around-EMB wobble, 6.44″ theory)
86
+ [[0, 0, 1, 0, -1, 0], [0, 0, 0, 0, 0, 0], -0.0841, -11.6309],
87
+ [[0, 0, 2, 0, -2, 0], [0, 0, -1, 0, 0, 0], -0.3235, 8.2645],
88
+ [[0, 0, 1, -1, 0, 0], [0, 0, 0, -1, 0, 0], -2.9917, -6.8503],
89
+ [[0, 0, 2, -2, 0, 0], [0, 0, -1, 0, 0, 0], -6.2197, -2.1203],
90
+ [[0, 2, -2, 0, 0, 0], [0, 0, 0, 0, 0, 0], 0.0199, -6.4326],
91
+ [[0, 0, 1, 0, -2, 0], [0, 0, 1, 0, 0, 0], 1.7254, 4.5991],
92
+ [[0, 1, -1, 0, 0, 0], [0, 0, 0, 0, 0, 0], 0.1362, 4.8325],
93
+ [[0, 0, 1, 0, -2, 0], [0, 0, 0, 0, 1, 0], 1.1469, 4.2788],
94
+ [[0, 0, 1, 0, -2, 0], [0, 0, 0, 0, 0, 0], -3.8890, 1.2230],
95
+ [[0, 0, 2, 0, -2, 0], [0, 0, 0, 0, 0, 0], 3.3414, 1.5744],
96
+ [[0, 0, 1, 0, -1, 0], [0, 0, 0, 0, -1, 0], -2.6985, -1.2967],
97
+ [[0, 3, -4, 0, 0, 0], [0, 0, 0, 0, 0, 0], 2.9374, -0.5408],
98
+ [[0, 0, 1, 0, -1, 0], [0, 0, -1, 0, 0, 0], -2.5406, -0.1812],
99
+ [[0, 0, 2, -2, 0, 0], [0, 0, 0, 0, 0, 0], 1.3755, -2.0301],
100
+ [[0, 0, 2, -3, 0, 0], [0, 0, 0, 1, 0, 0], -2.2367, -0.4398],
101
+ [[0, 0, -1, 2, 0, 0], [0, 0, 0, 0, 0, 0], 1.8705, 0.3499],
102
+ [[0, 3, -3, 0, 0, 0], [0, -1, 0, 0, 0, 0], -1.3492, 1.2893],
103
+ [[0, 0, 2, 0, -3, 0], [0, 0, 0, 0, 1, 0], 1.5453, 0.1289],
104
+ [[0, 2, -3, 0, 0, 0], [0, 0, 0, 0, 0, 0], 1.3344, -0.4621],
105
+ [[0, 3, -3, 0, 0, 0], [0, 0, -1, 0, 0, 0], -0.0755, -1.4049],
106
+ [[0, 0, -2, 4, 0, 0], [0, 0, 0, 0, 0, 0], 0.2780, 1.2111],
107
+ [[0, 0, -1, 2, 0, 0], [0, 0, -1, 0, 0, 0], -0.7161, 0.8440],
108
+ [[0, 3, -4, 0, 0, 0], [0, 0, -1, 0, 0, 0], 0.9894, 0.3587],
109
+ [[0, 0, 2, 0, -3, 0], [0, 0, 0, 0, 0, 0], 0.0063, 1.0463],
110
+ [[0, 2, -3, 0, 0, 0], [0, 0, 1, 0, 0, 0], 0.9834, 0.1318],
111
+ [[0, 0, 2, -3, 0, 0], [0, 0, 0, -1, 0, 0], 0.4834, 0.8318],
112
+ [[0, 1, -1, 0, 0, 0], [0, -1, 0, 0, 0, 0], -0.5335, 0.5905],
113
+ [[0, 0, 2, -2, 0, 0], [0, 0, 0, -1, 0, 0], 0.5371, -0.5685],
114
+ [[0, 0, 2, -3, 0, 0], [0, 0, 0, 0, 0, 0], -0.7373, -0.0880],
115
+ [[0, 2, -2, 0, 0, 0], [0, 0, -1, 0, 0, 0], 0.0163, -0.7372],
116
+ [[0, 0, -1, 2, 0, 0], [0, 0, 0, -1, 0, 0], -0.0119, 0.6500],
117
+ [[0, 0, 2, 0, -2, 0], [0, 0, 0, 0, 1, 0], 0.0023, -0.6287],
118
+ [[0, 0, 1, 0, -1, 0], [0, 0, 1, 0, 0, 0], 0.5714, -0.1358],
119
+ [[0, 0, -2, 4, 0, 0], [0, 0, -1, 0, 0, 0], -0.5046, 0.2858],
120
+ [[0, 0, 1, 0, -1, 0], [0, 0, 0, 0, 1, 0], -0.5567, 0.1036],
121
+ [[0, 2, -3, 0, 0, 0], [0, 0, -1, 0, 0, 0], 0.5469, -0.0167],
122
+ [[0, 2, -3, 0, 0, 0], [0, 1, 0, 0, 0, 0], -0.4309, 0.2809],
123
+ [[0, 0, 2, 0, -2, 0], [0, 0, 0, 0, -1, 0], 0.2206, -0.4616],
124
+ [[0, 3, -4, 0, 0, 0], [0, -1, 0, 0, 0, 0], -0.4549, -0.2097],
125
+ [[0, 0, 2, 0, -3, 0], [0, 0, -1, 0, 0, 0], 0.2809, 0.3489],
126
+ [[0, 0, 1, -1, 0, 0], [0, 0, 0, 0, 0, 0], 0.2863, 0.3116],
127
+ [[0, 0, 2, -3, 0, 0], [0, 0, -1, 0, 0, 0], -0.4141, -0.0359],
128
+ [[0, 0, 1, 0, 0, -1], [0, 0, 0, 0, 0, 0], 0.0863, -0.4015],
129
+ [[0, 0, -1, 2, 0, 0], [0, 0, 0, 1, 0, 0], -0.2200, 0.3372],
130
+ [[0, 1, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], -0.2017, 0.2910],
131
+ [[0, 2, -2, 0, 0, 0], [0, -1, 0, 0, 0, 0], 0.2499, -0.1777],
132
+ [[0, 0, 1, 0, -2, 0], [0, 0, 0, 0, -1, 0], -0.2254, 0.1676],
133
+ [[0, 3, -3, 0, 0, 0], [0, 0, 0, 0, 0, 0], -0.1859, -0.1961],
134
+ [[0, 0, 1, 0, 0, -1], [0, 0, 0, 0, 0, 1], -0.0710, 0.2499],
135
+ [[0, 0, -2, 4, 0, 0], [0, 0, 0, -1, 0, 0], 0.2125, -0.1175],
136
+ [[0, 0, 1, 0, 0, -1], [0, 0, -1, 0, 0, 0], -0.0460, 0.2379],
137
+ [[0, 3, -4, 0, 0, 0], [0, 1, 0, 0, 0, 0], -0.1410, 0.1566],
138
+ [[0, 0, 1, -1, 0, 0], [0, 0, 0, 1, 0, 0], -0.0476, 0.1897],
139
+ [[0, 2, -2, 0, 0, 0], [0, 0, 1, 0, 0, 0], -0.1559, -0.1155],
140
+ [[0, 0, 2, 0, -3, 0], [0, 0, 1, 0, 0, 0], 0.1631, -0.0164],
141
+ [[0, 1, -1, 0, 0, 0], [0, 1, 0, 0, 0, 0], 0.1307, -0.0678],
142
+ [[0, 0, 2, -3, 0, 0], [0, 0, 1, 0, 0, 0], 0.1331, -0.0291],
143
+ [[0, 0, -2, 4, 0, 0], [0, 0, 1, 0, 0, 0], 0.1227, 0.0482],
144
+ [[0, 0, 2, 0, 0, -2], [0, 0, 0, 0, 0, 0], -0.0388, 0.1044],
145
+ [[0, 0, 1, 0, 0, -1], [0, 0, 0, 0, 0, -1], -0.0309, 0.1027],
146
+ [[0, 0, 1, 0, -2, 0], [0, 0, -1, 0, 0, 0], -0.0741, 0.0308],
147
+ [[0, 0, 2, 0, -3, 0], [0, 0, 0, 0, -1, 0], 0.0102, 0.0783],
148
+ [[0, 1, -1, 0, 0, 0], [0, 0, 1, 0, 0, 0], -0.0026, 0.0765],
149
+ [[0, 0, 2, 0, -2, 0], [0, 0, 1, 0, 0, 0], -0.0084, 0.0686],
150
+ [[0, 3, -4, 0, 0, 0], [0, 0, 1, 0, 0, 0], 0.0273, -0.0576],
151
+ [[0, 0, 1, -1, 0, 0], [0, 0, 1, 0, 0, 0], 0.0570, 0.0117],
152
+ [[0, 0, 2, -2, 0, 0], [0, 0, 0, 1, 0, 0], 0.0426, -0.0336],
153
+ [[0, 0, 1, -1, 0, 0], [0, 0, -1, 0, 0, 0], -0.0335, -0.0395],
65
154
  ];
66
155
 
156
+ /** The declared-fitted semiannual leftover (header (iii)): +1.42″ sin 2lE. */
157
+ const FITTED_2LE_ARCSEC = 1.42;
158
+
67
159
  const D2R = Math.PI / 180;
68
160
 
69
161
  /**
70
- * Planetary-completion correction to the finder Sun longitude.
71
- * @param {number} T - Julian centuries TT from J2000
72
- * @returns {number} degrees SUBTRACT from the finder Sun longitude
162
+ * @param {{ embWobbleArcsec: number }} opts - the DERIVED Earth-around-EMB
163
+ * wobble amplitude a_M·μ/AU in arcsec = 1/(1+M_E/M_M)); computed
164
+ * from live constants by the model wiring so it tracks the constants.
165
+ * @returns {{ sunPlanetaryCompletionDeg: (T: number) => number }}
73
166
  */
74
- function sunPlanetaryCompletionDeg(T) {
75
- const lV = (ARG_RATES.lV[0] + ARG_RATES.lV[1] * T) * D2R;
76
- const lE = (ARG_RATES.lE[0] + ARG_RATES.lE[1] * T) * D2R;
77
- const lM = (ARG_RATES.lM[0] + ARG_RATES.lM[1] * T) * D2R;
78
- const lJ = (ARG_RATES.lJ[0] + ARG_RATES.lJ[1] * T) * D2R;
79
- const D = (ARG_RATES.D[0] + ARG_RATES.D[1] * T) * D2R;
80
- let arcsec = 0;
81
- for (const [kV, kE, kM, kJ, kD, cA, sA] of TERMS) {
82
- const th = kV * lV + kE * lE + kM * lM + kJ * lJ + kD * D;
83
- arcsec += cA * Math.cos(th) + sA * Math.sin(th);
167
+ function createSunPlanetaryCompletion({ embWobbleArcsec }) {
168
+ if (!Number.isFinite(embWobbleArcsec)) {
169
+ throw new Error('createSunPlanetaryCompletion: embWobbleArcsec must be a finite number (derived a_M·μ/AU in arcsec)');
84
170
  }
85
- return arcsec / 3600;
171
+ /**
172
+ * Planetary-completion correction to the finder Sun longitude.
173
+ * @param {number} T - Julian centuries TT from J2000
174
+ * @returns {number} degrees — SUBTRACT from the finder Sun longitude
175
+ */
176
+ function sunPlanetaryCompletionDeg(T) {
177
+ const l = new Float64Array(6), M = new Float64Array(6);
178
+ for (let i = 0; i < 6; i++) {
179
+ l[i] = (ARG_L0[i] + ARG_L1[i] * T) * D2R;
180
+ M[i] = l[i] - PERI[i] * D2R;
181
+ }
182
+ let table = 0;
183
+ for (const [kl, kM, cA, sA] of TERMS) {
184
+ let th = 0;
185
+ for (let i = 0; i < 6; i++) th += kl[i] * l[i] + kM[i] * M[i];
186
+ table += cA * Math.cos(th) + sA * Math.sin(th);
187
+ }
188
+ const D = (ARG_D[0] + ARG_D[1] * T) * D2R;
189
+ const arcsec = -table - embWobbleArcsec * Math.sin(D)
190
+ + FITTED_2LE_ARCSEC * Math.sin(2 * l[2]);
191
+ return arcsec / 3600;
192
+ }
193
+ return { sunPlanetaryCompletionDeg };
86
194
  }
87
195
 
88
- module.exports = { sunPlanetaryCompletionDeg };
196
+ /** sha256/16 of JSON.stringify(FITTED_COEFFICIENTS.SUN_LONGITUDE_HARMONICS)
197
+ * at derivation time — the matched-pair fingerprint asserted by test:model.
198
+ * Unchanged from v1: SUN_HARMONICS did not move in the D2 landing. */
199
+ const PAIRED_SUN_HARMONICS_SHA256 = 'e2cf42e9770c9e0a';
200
+
201
+ module.exports = { createSunPlanetaryCompletion, PAIRED_SUN_HARMONICS_SHA256 };
package/src/model.js CHANGED
@@ -31,7 +31,8 @@ import { createMoonMonthChain } from './moon/month-chain.cjs';
31
31
  import { createChainCycleIntegrator } from './chain-cycles/index.cjs';
32
32
  import { createMoonArguments, jdToDecimalYear } from './moon/arguments.cjs';
33
33
  import { createMoonSeries } from './moon/series.cjs';
34
- import { sunPlanetaryCompletionDeg } from './eclipse/sun-planetary-completion.cjs';
34
+ import { createSunPlanetaryCompletion } from './eclipse/sun-planetary-completion.cjs';
35
+ import { moonSeriesExtensionDeg } from './moon/series-extension.cjs';
35
36
  import { createEclipseFinders } from './eclipse/finders.cjs';
36
37
  import { createBesselian } from './eclipse/besselian.cjs';
37
38
  import { driver2PeriodSecondsAtAge } from './planets/orbit-chain.cjs';
@@ -828,6 +829,63 @@ export function assembleModel(C, F) {
828
829
  },
829
830
  });
830
831
 
832
+ // ── E4: the framework-native Sun (§12i item 11 — the 3b landing) ─────────
833
+ // Assembled from the model's own laws, ZERO fitted sun constants:
834
+ // e(t) = the H/16 eccentricity-channel law + the derived H/3
835
+ // inclination-coupling imprint (amplitude base/2, lattice phase,
836
+ // J2000-anchored difference form) — the osculating decomposition:
837
+ // osculating e = H/16 channel + inclination coupling.
838
+ // L(t) = L0 + the mean tropical rate + the f(Y) drift SHAPE only,
839
+ // ∫(rate_SI(y) − rate_SI(2000)) dy: the rate ANCHOR stays the mean
840
+ // year (eclipse-endorsed); the drift is the Step 6d year-harmonic
841
+ // claim in SI/TT (year-in-days × LOD — the LOD-day part of the
842
+ // raw drift is UT-vs-TT and stays ΔT's job). Trapezoid table,
843
+ // 10-yr steps over −3000..3000; outside, the drift freezes at the
844
+ // edge (rate reverts to linear — the finder domain is the corpus
845
+ // era).
846
+ // ϖ(t) = the shipped H/16 perihelion law (earthPerihelionDeg).
847
+ // Measured (tools/explore/e3b-native-sun.mjs): beats the Meeus Ch. 25
848
+ // basis on JPL all-phase (0.95″ vs 1.28″ scatter) and on ancient-corpus
849
+ // timing structure (0.37 vs 0.50 min detrended vs Meeus-T²); required-ΔT
850
+ // shift 2–4 min ≈ 0.23σ of Stephenson scatter (lunar bias improves); the
851
+ // D2 completion table is unchanged (residual 2lE re-fit ≈ 0.1″ — noise).
852
+ const sunL0Deg = C.earthOrbital.sunMeanLongitudeJ2000_deg;
853
+ const sunTropicalRateDegPerCy = 360 * julianCenturyDays / meanSolarYearDays;
854
+ /** @param {number} year @returns {number} */
855
+ const sunEccentricityAt = (year) => {
856
+ const cos3 = -Math.cos(phaseRadians(balancedYear, year, 3));
857
+ const cos3J2000 = -Math.cos(phaseRadians(balancedYear, 2000, 3));
858
+ return eccentricityAt(year) + (eccentricityBase / 2) * (cos3 - cos3J2000);
859
+ };
860
+ const sunMeanLongitudeDegAt = (() => {
861
+ const Y_LO = -3000, Y_HI = 3000, STEP = 10;
862
+ /** @type {Float64Array|null} */
863
+ let cum = null;
864
+ /** @param {number} year @returns {number} */
865
+ const rateSIYr = (year) => 360 * (365.25 * 86400)
866
+ / (tropicalYearDirectDays(year)
867
+ * (deepLod.lodSecondsAtAge(yearToTMa(year)) ?? meanLengthOfDay));
868
+ return /** @param {number} year @returns {number} */ (year) => {
869
+ if (cum === null) {
870
+ const n = (Y_HI - Y_LO) / STEP + 1;
871
+ cum = new Float64Array(n);
872
+ const ref = rateSIYr(2000);
873
+ let prev = rateSIYr(Y_LO) - ref;
874
+ for (let i = 1; i < n; i++) {
875
+ const next = rateSIYr(Y_LO + i * STEP) - ref;
876
+ cum[i] = cum[i - 1] + 0.5 * (prev + next) * STEP;
877
+ prev = next;
878
+ }
879
+ const at2000 = cum[(2000 - Y_LO) / STEP];
880
+ for (let i = 0; i < n; i++) cum[i] -= at2000;
881
+ }
882
+ const x = (Math.min(Math.max(year, Y_LO), Y_HI) - Y_LO) / STEP;
883
+ const i = Math.min(Math.floor(x), cum.length - 2);
884
+ const drift = cum[i] + (x - i) * (cum[i + 1] - cum[i]);
885
+ return sunL0Deg + sunTropicalRateDegPerCy * (year - 2000) / 100 + drift;
886
+ };
887
+ })();
888
+
831
889
  // Eclipse finders — wired like the engine probe (tools/verify/
832
890
  // eclipse-audit.js). The finder axis is JD(UT): the series wrapper applies
833
891
  // UT→TT internally. Ground-track/umbra paths deliberately absent: the
@@ -840,6 +898,13 @@ export function assembleModel(C, F) {
840
898
  deltaTSecondsAt: frameworkDeltaTSecondsAtJD,
841
899
  getSynodicMonthDays: () => moonSynodicMonthDays,
842
900
  getSunDistanceKm: () => currentAUDistance,
901
+ frameworkSun: {
902
+ sunMeanLongitudeJ2000Deg: sunL0Deg,
903
+ tropicalRateDegPerCy: sunTropicalRateDegPerCy,
904
+ eccentricityAt: sunEccentricityAt,
905
+ perihelionLongitudeDegAt: earthPerihelionDeg,
906
+ meanLongitudeDegAt: sunMeanLongitudeDegAt,
907
+ },
843
908
  constants: {
844
909
  rEarthMetres: (C.bodyDiametersKm.earth / 2) * 1000,
845
910
  moonDiameterKm: C.bodyDiametersKm.moon,
@@ -856,6 +921,11 @@ export function assembleModel(C, F) {
856
921
  // curve, the same convention every other ΔT consumer reads). Both axis
857
922
  // conventions are measured against the NASA path-table centerlines —
858
923
  // see eclipse/besselian.cjs.
924
+ // Derived Earth-around-EMB wobble for the Sun completion: a_M·μ/AU,
925
+ // μ = 1/(1+M_E/M_M) — 6.4399″ at current constants, tracks them live.
926
+ const embWobbleArcsec = (moonDistanceKm / (MASS_RATIO_EARTH_MOON + 1) / currentAUDistance)
927
+ * (648000 / Math.PI);
928
+ const { sunPlanetaryCompletionDeg } = createSunPlanetaryCompletion({ embWobbleArcsec });
859
929
  const besselian = createBesselian({
860
930
  moonFullAtDaysTT: /** @param {number} dDaysTT */ (dDaysTT) => {
861
931
  const ev = moonSeries.sceneEvalAt(dDaysTT);
@@ -863,6 +933,7 @@ export function assembleModel(C, F) {
863
933
  },
864
934
  sunLonDegAt: /** @param {number} jdUT */ (jdUT) => eclipseFinders.sunLonDegAt(jdUT),
865
935
  sunCompletionDeg: sunPlanetaryCompletionDeg,
936
+ moonExtensionAt: moonSeriesExtensionDeg,
866
937
  deltaTSecondsAt: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
867
938
  obliquityDegAt: obliquityDeg,
868
939
  eccentricityAt,
@@ -949,6 +1020,18 @@ export function assembleModel(C, F) {
949
1020
  findLunarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findLunarEclipsesInRange(jdStart, jdEnd),
950
1021
  findSolarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findSolarEclipsesInRange(jdStart, jdEnd),
951
1022
  deltaTSecondsAtJD: frameworkDeltaTSecondsAtJD,
1023
+ // E4 — the framework-native Sun deps, exported so the OTHER finder
1024
+ // construction sites (tools/verify/eclipse-audit.js, the browser
1025
+ // _eclipse twins) spread the SAME assembly into their own
1026
+ // createEclipseFinders call instead of triplicating it (the
1027
+ // three-runtimes rule — cf. recession-history):
1028
+ frameworkSunDeps: Object.freeze({
1029
+ sunMeanLongitudeJ2000Deg: sunL0Deg,
1030
+ tropicalRateDegPerCy: sunTropicalRateDegPerCy,
1031
+ eccentricityAt: sunEccentricityAt,
1032
+ perihelionLongitudeDegAt: earthPerihelionDeg,
1033
+ meanLongitudeDegAt: sunMeanLongitudeDegAt,
1034
+ }),
952
1035
  // 20.3g location tier (see eclipse/besselian.cjs):
953
1036
  umbraGroundAtJD: /** @param {number} jd @returns {{latDeg: number, lonDeg: number} | null} */ (jd) => besselian.umbraGroundAt(jd),
954
1037
  solarLocalCircumstances: /** @param {number} jdGreatest @param {number} latDeg @param {number} lonDeg */ (jdGreatest, latDeg, lonDeg) => besselian.localCircumstances(jdGreatest, latDeg, lonDeg),
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Moon series extension — the DERIVED Delaunay tail beyond the shipped
3
+ * Meeus-60 head (Stage D2 Phase A, plan §12i item 10).
4
+ *
5
+ * DERIVATION, not fit: the amplitudes come from the Stage-D1 3-body lab
6
+ * (tools/explore/derive-meeus-amplitudes.js — RK4 on framework constants,
7
+ * IC calibration to the free elements), joint-LSQ over [Meeus-60 head +
8
+ * extended Delaunay catalog] on a 120-yr window. Head fidelity
9
+ * 100.00–100.03% (the derivation reproduces the shipped head, so the
10
+ * tail is the same physics one order deeper); dt-halving drift 0.000″.
11
+ * Extraction: tools/explore/d2-extension-extraction.mjs.
12
+ *
13
+ * JPL enters only as OUT-OF-SAMPLE VALIDATION (960 all-phase epochs):
14
+ * shipped-Moon residual λ 3.68″ → 3.12″ (predicted 3.08″) and
15
+ * β 1.04″ → 0.65″ (predicted 0.66″) with the extension ADDED; the
16
+ * sign-flipped control degrades both — the integrator predicted reality.
17
+ * Ships JOINTLY with the derived Sun completion v2
18
+ * (eclipse/sun-planetary-completion.cjs): the Moon-only form degrades
19
+ * the NASA centerline reference set (its tail was cancelling Sun-side
20
+ * leftover tones there), and the pre-registered acceptance was all four
21
+ * gates, never a subset.
22
+ *
23
+ * Arguments are the instrument-grade Delaunay polynomials (IAU/Meeus
24
+ * class, degrees/Julian-century TT) — phase carriers only, same
25
+ * epistemic status as the Sun completion's argument literals (their
26
+ * framework-native derivation is research item 20.3e).
27
+ *
28
+ * Sign convention: sine terms, ADDED to the full-series Moon longitude
29
+ * and latitude (the validated sense; see besselian.cjs bodiesAt).
30
+ *
31
+ * THE A2 PLANETARY TAIL (the second derived section below): the Moon's
32
+ * direct planetary perturbations, derived from epoch-phased twin 8-body
33
+ * integrations (full = planets+J2 vs base = J2-only; planet phases from
34
+ * the engine graph, the Moon at its true epoch Delaunay phases; fitted
35
+ * on the run's own arguments with a main-problem absorber catalog,
36
+ * absorbers discarded — instrument
37
+ * tools/explore/d2-planetary-moon-epoch.mjs). Cross-validated: V−E
38
+ * +0.849″ sin (independent JPL fit: +0.85), E−J −0.681″ (JPL: −0.69);
39
+ * content 0.896″. JPL out-of-sample (960 all-phase epochs): λ scatter
40
+ * 3.12″ → 3.03″ (the extraction predicted 2.98); the sign-flipped
41
+ * control degrades to 3.47″; the syzygy fleet improves 3.99″ → 3.82″
42
+ * in EVERY era. The β rows (0.074″ content) measurably do NOT help and
43
+ * are not shipped. RECORDED TENSION (owner-accepted at shipping): the
44
+ * 13-event NASA centerline scoreboard moves 2.22″ → 2.68″ mean — the
45
+ * A1-class correlated-subsample effect (the tracked events sit −1.6″
46
+ * below the +1.4″ fleet mean and sample the near-annual arguments at
47
+ * correlated phases; the era-split refutes anchor double-counting and
48
+ * the modern era improves most, 3.57 → 3.34). The ≤8″ api gate holds
49
+ * (max 6.2″). Coefficients are stored with the run→real argument
50
+ * mapping already applied (the extraction's heliocentric-planet vs
51
+ * geocentric-sun π-offset is folded into the signs).
52
+ */
53
+
54
+ 'use strict';
55
+
56
+ /** Delaunay argument polynomials [deg at J2000, deg/cy, deg/cy²] TT. */
57
+ const DELAUNAY = {
58
+ D: [297.8501921, 445267.1114034, -0.0018819], // mean elongation
59
+ M: [357.5291092, 35999.0502909, -0.0001536], // Sun mean anomaly
60
+ Mp: [134.9633964, 477198.8675055, 0.0087414], // Moon mean anomaly
61
+ F: [93.2720950, 483202.0175233, -0.0036539], // argument of latitude
62
+ };
63
+
64
+ /** Longitude tail: [kD, kM, kMp, kF, sin″] — 47 derived terms ≥ 0.1″
65
+ * (RMS content 2.02″). */
66
+ const LON_TERMS = [
67
+ [2, 0, 1, 2, -0.9900],
68
+ [2, 0, -4, 0, 0.9465],
69
+ [2, -2, 1, 0, 0.7524],
70
+ [0, 1, -3, 0, -0.6704],
71
+ [4, 1, -1, 0, -0.6323],
72
+ [1, 0, 2, 0, -0.5864],
73
+ [1, 0, 0, -2, -0.5813],
74
+ [2, 0, -2, -2, -0.5631],
75
+ [1, -1, 0, 0, -0.5617],
76
+ [0, 1, 3, 0, -0.5455],
77
+ [2, 0, -2, 2, -0.5253],
78
+ [2, -1, -3, 0, 0.4674],
79
+ [2, 0, 2, -2, -0.4625],
80
+ [2, -1, -1, 2, -0.4271],
81
+ [0, 1, 0, 2, 0.4173],
82
+ [6, 0, -1, 0, 0.3954],
83
+ [2, -1, 1, -2, -0.3876],
84
+ [2, -1, 0, 2, -0.3816],
85
+ [2, 2, -1, -2, -0.3654],
86
+ [2, 2, 1, -2, -0.3615],
87
+ [4, 1, -2, 0, -0.3575],
88
+ [0, 0, 3, 2, -0.3281],
89
+ [4, -2, -1, 0, 0.3106],
90
+ [0, 1, -1, -2, 0.3016],
91
+ [2, -2, -2, 0, 0.2964],
92
+ [4, 0, -1, -2, 0.2963],
93
+ [2, 1, 2, 0, -0.2897],
94
+ [4, 1, 0, 0, -0.2887],
95
+ [4, -1, 1, 0, 0.2833],
96
+ [0, 1, 1, 2, 0.2643],
97
+ [2, 2, -2, 0, -0.2635],
98
+ [1, 1, -2, 0, 0.2569],
99
+ [1, 0, 0, 2, 0.2524],
100
+ [4, 0, 2, 0, 0.2190],
101
+ [4, 0, -1, 2, -0.2007],
102
+ [0, 2, -2, 0, -0.1813],
103
+ [0, 0, 3, -2, -0.1784],
104
+ [2, 2, 0, 0, -0.1684],
105
+ [2, 0, -1, -2, 0.1634],
106
+ [4, -2, 0, 0, 0.1549],
107
+ [2, 2, -3, 0, 0.1545],
108
+ [1, -1, -1, 0, -0.1479],
109
+ [2, 1, -3, 0, 0.1290],
110
+ [6, 0, 0, 0, 0.1268],
111
+ [1, 0, -3, 0, -0.1252],
112
+ [2, 0, 2, 2, -0.1236],
113
+ [1, -1, 1, 0, -0.1225],
114
+ ];
115
+
116
+ /** Latitude tail: [kD, kM, kMp, kF, sin″] — 30 derived terms ≥ 0.1″
117
+ * (RMS content 0.80″). */
118
+ const LAT_TERMS = [
119
+ [4, -1, -1, 1, 0.3390],
120
+ [2, 0, -1, -3, 0.3282],
121
+ [2, -2, -1, 1, 0.3148],
122
+ [0, 1, 2, -1, -0.3050],
123
+ [0, 1, -2, 1, -0.2988],
124
+ [2, 0, 1, -3, -0.2919],
125
+ [2, -2, -1, -1, 0.2685],
126
+ [0, 0, 4, 1, 0.2636],
127
+ [2, 0, -3, 1, 0.2526],
128
+ [2, 0, -1, 3, -0.2447],
129
+ [2, 1, 1, 1, -0.2369],
130
+ [4, 0, 1, 1, 0.2128],
131
+ [2, 2, 0, -1, -0.1749],
132
+ [4, 1, -1, -1, -0.1737],
133
+ [4, -1, 0, 1, 0.1581],
134
+ [2, 0, 3, -1, 0.1465],
135
+ [0, 0, 2, -3, -0.1461],
136
+ [2, 0, 0, 3, -0.1444],
137
+ [1, 0, -1, 1, 0.1387],
138
+ [2, 0, 3, 1, 0.1381],
139
+ [2, 0, -4, -1, 0.1335],
140
+ [2, -1, 2, -1, 0.1283],
141
+ [2, -1, 2, 1, 0.1241],
142
+ [0, 2, -1, -1, -0.1208],
143
+ [0, 0, 2, 3, -0.1178],
144
+ [4, 1, 0, -1, -0.1128],
145
+ [2, 2, -1, -1, -0.1127],
146
+ [1, 0, -2, -1, -0.1093],
147
+ [2, 2, -1, 1, -0.1044],
148
+ [1, 1, 1, 1, 0.1014],
149
+ ];
150
+
151
+ /** Planetary mean-longitude polynomials, degrees per Julian century TT
152
+ * (IAU-class instrument literals — same carriers as the Sun completion).
153
+ * Order: Venus, Earth (EMB), Mars, Jupiter, Saturn. */
154
+ const PLANET_L0 = [181.979801, 100.466457, 355.433000, 34.351519, 50.077444];
155
+ const PLANET_L1 = [58517.815676, 36000.769780, 19141.696300, 3036.302389, 1223.511013];
156
+
157
+ /**
158
+ * The A2 planetary longitude tail:
159
+ * [[kV, kE, kMa, kJ, kSa, kD, kMp], cos″, sin″] — 15 derived terms
160
+ * (content 0.896″), run→real argument mapping folded into the signs.
161
+ * @type {Array<[number[], number, number]>}
162
+ */
163
+ const PLANETARY_LON_TERMS = [
164
+ [[1, -1, 0, 0, 0, 0, 0], -0.0056, -0.8488],
165
+ [[2, -2, 0, 0, 0, 0, 0], -0.0034, 0.3166],
166
+ [[3, -3, 0, 0, 0, 0, 0], -0.0051, -0.0387],
167
+ [[0, 1, 0, -1, 0, 0, 0], 0.0325, 0.6814],
168
+ [[0, 2, 0, -2, 0, 0, 0], -0.0084, -0.1835],
169
+ [[0, 2, -2, 0, 0, 0, 0], 0.0002, 0.2205],
170
+ [[0, 1, 0, 0, -1, 0, 0], 0.0078, 0.0463],
171
+ [[1, -1, 0, 0, 0, 2, 0], -0.0020, -0.0308],
172
+ [[1, -1, 0, 0, 0, -2, 0], 0.0045, -0.1193],
173
+ [[0, 1, 0, -1, 0, 2, 0], 0.0014, 0.0322],
174
+ [[0, 1, 0, -1, 0, -2, 0], 0.0161, 0.2141],
175
+ [[1, -1, 0, 0, 0, 0, 1], 0.0023, -0.1929],
176
+ [[1, -1, 0, 0, 0, 0, -1], -0.0029, -0.2306],
177
+ [[0, 1, 0, -1, 0, 0, 1], 0.0059, 0.1729],
178
+ [[0, 1, 0, -1, 0, 0, -1], -0.0077, 0.2235],
179
+ ];
180
+
181
+ const D2R = Math.PI / 180;
182
+
183
+ /**
184
+ * Derived series-extension correction to the full-series Moon:
185
+ * the Delaunay tail plus the A2 planetary tail (longitude only).
186
+ * @param {number} T - Julian centuries TT from J2000
187
+ * @returns {{dLonDeg: number, dLatDeg: number}} degrees — ADD to the
188
+ * full-series Moon longitude/latitude
189
+ */
190
+ function moonSeriesExtensionDeg(T) {
191
+ const poly = /** @param {number[]} p */ (p) => (p[0] + (p[1] + p[2] * T) * T) * D2R;
192
+ const D = poly(DELAUNAY.D), M = poly(DELAUNAY.M), Mp = poly(DELAUNAY.Mp), F = poly(DELAUNAY.F);
193
+ let lon = 0, lat = 0;
194
+ for (const [kD, kM, kMp, kF, s] of LON_TERMS) lon += s * Math.sin(kD * D + kM * M + kMp * Mp + kF * F);
195
+ for (const [kD, kM, kMp, kF, s] of LAT_TERMS) lat += s * Math.sin(kD * D + kM * M + kMp * Mp + kF * F);
196
+ const l = new Float64Array(5);
197
+ for (let i = 0; i < 5; i++) l[i] = (PLANET_L0[i] + PLANET_L1[i] * T) * D2R;
198
+ for (const [k, cA, sA] of PLANETARY_LON_TERMS) {
199
+ const th = k[0] * l[0] + k[1] * l[1] + k[2] * l[2] + k[3] * l[3] + k[4] * l[4] + k[5] * D + k[6] * Mp;
200
+ lon += cA * Math.cos(th) + sA * Math.sin(th);
201
+ }
202
+ return { dLonDeg: lon / 3600, dLatDeg: lat / 3600 };
203
+ }
204
+
205
+ module.exports = { moonSeriesExtensionDeg };