@essrt/physics 1.2.0 → 1.3.1

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/README.md CHANGED
@@ -53,7 +53,7 @@ The canonical assembly, wired once inside the package:
53
53
  import { createModel, DEFAULT_CONSTANTS } from '@essrt/physics';
54
54
 
55
55
  const model = createModel();
56
- model.identity.modelVersion; // 'v10.0' + both content hashes
56
+ model.identity.modelVersion; // the shipped model identity (e.g. 'v11.0') + both content hashes
57
57
  model.epoch.hAtYear(2000 - 380e6); // 306189 — Devonian H (the Wells 1963 match)
58
58
  model.earth.obliquityDeg(2000); // 23.4393
59
59
  model.cardinal.jd(2000, 'SS'); // 2451716.575 — June solstice 2000 (USNO)
@@ -75,8 +75,13 @@ with their own formula), `identity` (version + hashes + citation DOI), `epoch`
75
75
  (H, LOD, α, ΔT, deep-time quantities at any year ±500 Myr), `earth`
76
76
  (obliquity, eccentricity, inclination, perihelion), `lengths` (year and
77
77
  day lengths), `cardinal` (solstice/equinox JD, RA, year length), `moon`
78
- (distance, sidereal month). Validation targets are refused as inputs
79
- a counterfactual cannot move the goalposts it is judged by.
78
+ (distance and months at epoch, plus the apparent-position chain on the
79
+ JD(UT) axis ecliptic longitude, latitude and distance from the shared
80
+ Meeus series over framework-native arguments), `eclipse` (geocentric
81
+ solar/lunar eclipse search over a JD window, greatest-eclipse convention),
82
+ `climate` (the L1 orbital-forcing formula) and `planets` (the Fibonacci-law
83
+ records and orientation at epoch). Validation targets are refused as
84
+ inputs — a counterfactual cannot move the goalposts it is judged by.
80
85
 
81
86
  ## Underneath: a parts library
82
87
 
@@ -88,7 +93,9 @@ browser. For custom wiring (different α channel, subset assembly), use
88
93
  the factories directly: the reference adapter is `tools/lib/` in the
89
94
  [public repository](https://github.com/dvansonsbeek/3d) — the hosted
90
95
  simulator and the project's verification gates run on it. A service
91
- surface (API/MCP) over `createModel()` is on the project roadmap.
96
+ surface over `createModel()` lives in the same repository
97
+ (`packages/api`, an OpenAPI 3.1 handler with zero runtime dependencies)
98
+ — build-only until the hosting decision.
92
99
 
93
100
  ## Provenance
94
101
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@essrt/physics",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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
- "modelVersion": "v10.0",
6
+ "modelVersion": "v11.0",
7
7
  "_note": "The model identity this package version ships (IP-unified-architecture \u00a710: package semver tracks the API axis; the model version + CONSTANTS_HASH/COEFFICIENTS_HASH in src/constants/ carry the scientific identity). The version-pinning gate enforces this pairing against public/input/model-version.json."
8
8
  },
9
9
  "type": "module",
@@ -49,6 +49,7 @@
49
49
  "./deltat/cycles": "./src/deltat/cycles.cjs",
50
50
  "./deltat/deep-time": "./src/deltat/deep-time.cjs",
51
51
  "./deltat/historical": "./src/deltat/historical.cjs",
52
+ "./deltat/recession-history": "./src/deltat/recession-history.cjs",
52
53
  "./climate/l1-orbital": "./src/climate/l1-orbital.cjs",
53
54
  "./eclipse/finders": "./src/eclipse/finders.cjs",
54
55
  "./reference/published-curves": "./src/reference/published-curves.cjs",
@@ -2,14 +2,14 @@
2
2
  // Gives the TypeScript website full type safety at the boundary (§2g) while
3
3
  // packages/physics stays JavaScript.
4
4
 
5
- export declare const CONSTANTS_HASH: "d36050ef216d137f";
5
+ export declare const CONSTANTS_HASH: "471a3f440b88cd86";
6
6
 
7
7
  export declare const MODEL_VERSION: string;
8
8
 
9
9
  export declare const PREPRINT_DOI: string;
10
10
 
11
11
  export declare const DEFAULT_CONSTANTS: {
12
- readonly hash: "d36050ef216d137f";
12
+ readonly hash: "471a3f440b88cd86";
13
13
  readonly additionalBodies: {
14
14
  "pluto": {
15
15
  "name": string;
@@ -137,6 +137,17 @@ export declare const DEFAULT_CONSTANTS: {
137
137
  "alphaClimateScalePerMille": number;
138
138
  "dtStackTaperFullHalfwidthYr": number;
139
139
  "dtStackTaperTotalHalfwidthYr": number;
140
+ "recessionRegime": {
141
+ "jointMa": number;
142
+ "knotAgesMa": number[];
143
+ "knotDistancesKm": number[];
144
+ "genesisMa": number;
145
+ "rocheLimitKm": number;
146
+ "solarOceanLeakBeta0": number;
147
+ "thermalPumpStartMa": number;
148
+ "thermalPumpEndMa": number;
149
+ "thermalPumpFactor": number;
150
+ };
140
151
  };
141
152
  readonly earth: {
142
153
  "earthtiltMean": number;
@@ -32,17 +32,17 @@
32
32
  * carry it so a counterfactual is reproducible (§2d).
33
33
  * @type {string}
34
34
  */
35
- export const CONSTANTS_HASH = "d36050ef216d137f";
35
+ export const CONSTANTS_HASH = "471a3f440b88cd86";
36
36
 
37
37
  /** Model version label — single source: public/input/model-version.json (§10 two-axis scheme). */
38
- export const MODEL_VERSION = "v10.0";
38
+ export const MODEL_VERSION = "v11.0";
39
39
 
40
40
  /** Canonical preprint DOI — single source: public/input/model-version.json. */
41
41
  export const PREPRINT_DOI = "10.21203/rs.3.rs-8758810/v4";
42
42
 
43
43
  /** @type {Readonly<Record<string, unknown>>} */
44
44
  export const DEFAULT_CONSTANTS = Object.freeze({
45
- hash: "d36050ef216d137f",
45
+ hash: "471a3f440b88cd86",
46
46
  additionalBodies: {
47
47
  "pluto": {
48
48
  "name": "Pluto",
@@ -181,7 +181,30 @@ export const DEFAULT_CONSTANTS = Object.freeze({
181
181
  "alpha4PerMa4": 1.3619800519e-16,
182
182
  "alphaClimateScalePerMille": -3.93e-7,
183
183
  "dtStackTaperFullHalfwidthYr": 300000,
184
- "dtStackTaperTotalHalfwidthYr": 400000
184
+ "dtStackTaperTotalHalfwidthYr": 400000,
185
+ "recessionRegime": {
186
+ "jointMa": 1000,
187
+ "knotAgesMa": [
188
+ 1400,
189
+ 1900,
190
+ 2450,
191
+ 3200,
192
+ 3900
193
+ ],
194
+ "knotDistancesKm": [
195
+ 337531.908,
196
+ 322949.311,
197
+ 322533.193,
198
+ 296189.703,
199
+ 275119.896
200
+ ],
201
+ "genesisMa": 4498,
202
+ "rocheLimitKm": 9439.643,
203
+ "solarOceanLeakBeta0": 0.2,
204
+ "thermalPumpStartMa": 1000,
205
+ "thermalPumpEndMa": 1307,
206
+ "thermalPumpFactor": 0.454
207
+ }
185
208
  },
186
209
  earth: {
187
210
  "earthtiltMean": 23.413527499101747,
@@ -64,19 +64,24 @@
64
64
  * @property {(year: number) => number} cycleLodSumAt - gated δLOD sum (incl. swing)
65
65
  * @property {(year: number) => number} swingLodAt - gated swing δLOD alone
66
66
  * @property {(year: number) => number} swingLodRateAt - gated analytic swing rate
67
+ * @property {(tMa: number) => number} [lEmAtAgeKgm2S] - OPTIONAL time-dependent
68
+ * Earth-Moon angular momentum (the Driver-1½ solar channels,
69
+ * recession-history.cjs). Absent → the J2000 constant, which the budget
70
+ * module equals exactly for t ≤ jointMa — the pure-twin convention.
67
71
  */
68
72
 
69
73
  /** @param {DeepTimeLodDeps} deps */
70
74
  function createDeepTimeLod(deps) {
71
75
  const K = deps.constants;
76
+ const lEmAt = deps.lEmAtAgeKgm2S ?? (() => K.lTotalEmKgm2S);
72
77
 
73
- /** Layer 1/2 mean LOD: LOD = 2π·I(t) / (L_total − L_moon(t)).
78
+ /** Layer 1/2 mean LOD: LOD = 2π·I(t) / (L_EM(t) − L_moon(t)).
74
79
  * @param {number} t_Ma @returns {number|null} seconds, null past tidal lock */
75
80
  function lodSecondsAtAge(t_Ma) {
76
81
  const a = deps.moonDistanceMetresAtAge(t_Ma);
77
82
  if (a <= 0 || a >= K.aLockMetres) return null;
78
83
  return (2 * Math.PI * (deps.moiFactorAtAge(t_Ma) * K.mEarthAloneKg * K.rEarthMetres * K.rEarthMetres)) /
79
- (K.lTotalEmKgm2S - K.mMoonAloneKg * Math.sqrt(K.gmEmM3PerS2 * a) * K.eFactorMoon);
84
+ (lEmAt(t_Ma) - K.mMoonAloneKg * Math.sqrt(K.gmEmM3PerS2 * a) * K.eFactorMoon);
80
85
  }
81
86
 
82
87
  /** Same LOD with an EXPLICIT α (the browser's "α at climate mean" curve).
@@ -86,7 +91,7 @@ function createDeepTimeLod(deps) {
86
91
  if (a <= 0 || a >= K.aLockMetres) return null;
87
92
  const iEarth = alpha * K.mEarthAloneKg * K.rEarthMetres * K.rEarthMetres;
88
93
  return (2 * Math.PI * iEarth) /
89
- (K.lTotalEmKgm2S - K.mMoonAloneKg * Math.sqrt(K.gmEmM3PerS2 * a) * K.eFactorMoon);
94
+ (lEmAt(t_Ma) - K.mMoonAloneKg * Math.sqrt(K.gmEmM3PerS2 * a) * K.eFactorMoon);
90
95
  }
91
96
 
92
97
  /** @param {number} t_Ma @returns {number|null} */
@@ -108,7 +113,15 @@ function createDeepTimeLod(deps) {
108
113
  function siderealYearSecondsAtAge(t_Ma) {
109
114
  if (t_Ma === 0) return K.meanSiderealYearJ2000Seconds;
110
115
  const mass_loss_fraction = K.solarMassLossFracPerYear * t_Ma * 1e6;
111
- return K.meanSiderealYearJ2000Seconds * (1 - 2 * mass_loss_fraction);
116
+ // Exact Kepler under the model's mass-history convention (a·M = const,
117
+ // a(t) = a₀·(1−Δm)): T ∝ M⁻² ⇒ T(t) = T₀·(1−Δm)² — the SAME Driver-2
118
+ // law the planet chains use (orbit-chain driver2PeriodSecondsAtAge).
119
+ // The previous (1 − 2Δm) was this law's first-order Taylor — the one
120
+ // form in the model inconsistent with it, and the entire source of the
121
+ // day-count invariant's 0.17 ppm residual: with the product form,
122
+ // H·(days/yr)·(AU₀/AU)² = TOTAL_DAYS_IN_H holds EXACTLY
123
+ // (tools/explore/deep-time-sensitivity.js §5; docs/99 §near-invariant).
124
+ return K.meanSiderealYearJ2000Seconds * (1 - mass_loss_fraction) * (1 - mass_loss_fraction);
112
125
  }
113
126
 
114
127
  /** @param {number} t_Ma @returns {number} tropical = sidereal · (1 − 13/H(t)) */
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Regime-aware lunar recession history + the solar angular-momentum
3
+ * channels (Driver 1½) — THE shared implementation.
4
+ *
5
+ * Background (docs/99 §"The ungated mid-Precambrian window", docs/106 §D,
6
+ * and the research record in tools/explore/{farhat-divergence-probe,
7
+ * regime-aware-recession-study,fit-regime-recession}.js): Farhat 2022's
8
+ * central result is that lunar recession followed a resonant STAIRCASE —
9
+ * a single smooth quartic anchored at the modern LLR rate, the gated
10
+ * 0–650 Ma era and the Roche crossing cannot follow it through the
11
+ * 1–3.5 Ga window (−8.9σ Joffre, −11.4σ Moodies). This module carries
12
+ * the regime-aware history:
13
+ *
14
+ * t ≤ jointMa — the calibrated quartic, BIT-IDENTICAL (the
15
+ * Wells/Wu-gated era; its α set is the calibrated
16
+ * effective closed-budget pair and is not touched).
17
+ * jointMa..genesisMa — monotone cubic Hermite (Fritsch–Carlson) through
18
+ * the fitted knots, value- AND slope-continuous at
19
+ * the joint (analytic polynomial derivative), ending
20
+ * at the rigid Roche limit at genesisMa.
21
+ * t ≥ genesisMa — clamped at the Roche value (the Moon does not
22
+ * exist beyond its genesis; boundary value, not an
23
+ * extrapolation).
24
+ *
25
+ * The solar channels (explicit ONLY beyond jointMa — inside the gated era
26
+ * they are absorbed by the α calibration, and applying them there without
27
+ * recalibration measurably breaks Wells; see the study's variant 3):
28
+ * leak τ_s = β₀ (a/a₀)⁶ · τ_moon — ocean solar tide (M²/AU⁶ ratio
29
+ * scaling of the semidiurnal torque)
30
+ * pump τ_t = f_p · (τ_moon + τ_s) inside [pumpStartMa, pumpEndMa] —
31
+ * the insolation-driven atmospheric thermal tide
32
+ * (Zahnle–Walker 1987; Bartlett–Stevenson 2016;
33
+ * Mitchell–Kirscher 2023). The FITTED f_p ≈ 0.45 over a ~300 Myr
34
+ * window (degenerate with 0.15 × 900 Myr — the data constrain
35
+ * the INTEGRATED pump, ~2σ preferred over zero, Zhou 2024's
36
+ * contrary reading honestly noted in the fit record).
37
+ *
38
+ * Fit provenance: tools/explore/fit-regime-recession.js — 11 anchors
39
+ * (Lantink/Joffre 2022 · Weeli Wolli · Zhou 2024 ×3 paired a+LOD ·
40
+ * Meyers–Malinverno 2018 · Nanfen 2023 · Moodies), all ≤1.3σ; genesis
41
+ * pinned at the shipped 4498 Ma Roche crossing (the genesis profile is
42
+ * flat — the data do not constrain it). The knot values ARE the shipped
43
+ * definition (metre-rounded at fit time, not truncated from a more
44
+ * precise source).
45
+ */
46
+
47
+ 'use strict';
48
+
49
+ /**
50
+ * @typedef {Object} RecessionRegime
51
+ * @property {number} jointMa - regime boundary; quartic below, spline above
52
+ * @property {number[]} knotAgesMa - interior knot ages (ascending)
53
+ * @property {number[]} knotDistancesKm - fitted knot distances
54
+ * @property {number} genesisMa - the Roche-crossing epoch
55
+ * @property {number} rocheLimitKm - the rigid Roche endpoint
56
+ */
57
+
58
+ /**
59
+ * @param {{
60
+ * aMoonNowMetres: number,
61
+ * alpha1PerMa: number, alpha3PerMa3: number, alpha4PerMa4: number,
62
+ * regime: RecessionRegime,
63
+ * }} deps
64
+ * @returns {{ distanceMetresAtAge: (tMa: number) => number }}
65
+ */
66
+ function createMoonRecessionHistory(deps) {
67
+ const { aMoonNowMetres: aNow, alpha1PerMa: a1, alpha3PerMa3: a3, alpha4PerMa4: a4 } = deps;
68
+ const R = deps.regime;
69
+
70
+ /** The calibrated quartic (identical expression to every consumer).
71
+ * @param {number} tMa @returns {number} */
72
+ const polyM = (tMa) => aNow * (1 + a1 * tMa + a3 * tMa * tMa * tMa + a4 * tMa * tMa * tMa * tMa);
73
+ /** Analytic slope of the quartic, m/Ma.
74
+ * @param {number} tMa @returns {number} */
75
+ const polySlopeM = (tMa) => aNow * (a1 + 3 * a3 * tMa * tMa + 4 * a4 * tMa * tMa * tMa);
76
+
77
+ // Knot chain: joint (value+slope from the quartic) → fitted knots → Roche.
78
+ const ts = [R.jointMa, ...R.knotAgesMa, R.genesisMa];
79
+ const vs = [polyM(R.jointMa), ...R.knotDistancesKm.map((km) => km * 1000), R.rocheLimitKm * 1000];
80
+ const n = ts.length;
81
+ const d = [];
82
+ for (let i = 0; i < n - 1; i++) d.push((vs[i + 1] - vs[i]) / (ts[i + 1] - ts[i]));
83
+ const m = new Array(n);
84
+ m[0] = polySlopeM(R.jointMa);
85
+ for (let i = 1; i < n - 1; i++) m[i] = (d[i - 1] * d[i] <= 0) ? 0 : (d[i - 1] + d[i]) / 2;
86
+ m[n - 1] = d[n - 2];
87
+ // Fritsch–Carlson monotonicity limiter — the history must be monotone
88
+ // into the past (the Moon only recedes on the mean).
89
+ for (let i = 0; i < n - 1; i++) {
90
+ if (d[i] === 0) { m[i] = 0; m[i + 1] = 0; continue; }
91
+ const a = m[i] / d[i], b = m[i + 1] / d[i];
92
+ const s = a * a + b * b;
93
+ if (s > 9) { const tau = 3 / Math.sqrt(s); m[i] = tau * a * d[i]; m[i + 1] = tau * b * d[i]; }
94
+ }
95
+
96
+ /** @param {number} tMa @returns {number} metres */
97
+ function distanceMetresAtAge(tMa) {
98
+ if (tMa <= R.jointMa) return polyM(tMa);
99
+ if (tMa >= R.genesisMa) return R.rocheLimitKm * 1000;
100
+ let i = 0;
101
+ while (i < n - 2 && tMa > ts[i + 1]) i++;
102
+ const h = ts[i + 1] - ts[i], s = (tMa - ts[i]) / h, s2 = s * s, s3 = s2 * s;
103
+ return (2 * s3 - 3 * s2 + 1) * vs[i] + (s3 - 2 * s2 + s) * h * m[i]
104
+ + (-2 * s3 + 3 * s2) * vs[i + 1] + (s3 - s2) * h * m[i + 1];
105
+ }
106
+
107
+ return { distanceMetresAtAge };
108
+ }
109
+
110
+ /**
111
+ * The solar angular-momentum channels: L_EM(t) integrated backward from
112
+ * J2000 with the ocean leak and the thermal-tide pump, explicit only
113
+ * beyond jointMa. Absent this module, every consumer's L_EM is the J2000
114
+ * constant — which remains the exact behaviour for t ≤ jointMa.
115
+ *
116
+ * @param {{
117
+ * lTotalJ2000KgM2S: number,
118
+ * mMoonAloneKg: number, gmEmM3PerS2: number, eFactorMoon: number,
119
+ * beta0: number,
120
+ * pumpStartMa: number, pumpEndMa: number, pumpFactor: number,
121
+ * jointMa: number, genesisMa: number,
122
+ * distanceMetresAtAge: (tMa: number) => number,
123
+ * stepMa?: number,
124
+ * }} deps
125
+ * @returns {{ lEmAtAgeKgm2S: (tMa: number) => number }}
126
+ */
127
+ function createSolarChannelBudget(deps) {
128
+ const STEP = deps.stepMa ?? 5;
129
+ /** @param {number} aM @returns {number} */
130
+ const lOrb = (aM) => deps.mMoonAloneKg * Math.sqrt(deps.gmEmM3PerS2 * aM) * deps.eFactorMoon;
131
+ const a0 = deps.distanceMetresAtAge(0);
132
+
133
+ /** @type {Float64Array|null} */
134
+ let table = null;
135
+ const build = () => {
136
+ const nSteps = Math.ceil(deps.genesisMa / STEP) + 1;
137
+ const t = new Float64Array(nSteps);
138
+ let L = deps.lTotalJ2000KgM2S;
139
+ for (let i = 0; i < nSteps; i++) {
140
+ t[i] = L;
141
+ const tMa = i * STEP;
142
+ const dLorb = lOrb(deps.distanceMetresAtAge(tMa)) - lOrb(deps.distanceMetresAtAge(tMa + STEP));
143
+ const mid = tMa + STEP / 2;
144
+ const leakDt = mid > deps.jointMa ? deps.beta0 * Math.pow(deps.distanceMetresAtAge(tMa) / a0, 6) * dLorb : 0;
145
+ const pumpDt = (mid > deps.pumpStartMa && mid < deps.pumpEndMa) ? deps.pumpFactor * (dLorb + leakDt) : 0;
146
+ L = L - (pumpDt - leakDt);
147
+ }
148
+ return t;
149
+ };
150
+
151
+ /** @param {number} tMa @returns {number} */
152
+ function lEmAtAgeKgm2S(tMa) {
153
+ // The channels are zero below jointMa by construction — return the
154
+ // constant EXACTLY there (no table interpolation), so the gated era
155
+ // stays bit-identical to the closed-budget engine (the 1-ULP fixture
156
+ // gates depend on this).
157
+ if (tMa <= deps.jointMa) return deps.lTotalJ2000KgM2S;
158
+ if (table === null) table = build();
159
+ const x = tMa / STEP;
160
+ const i = Math.min(Math.floor(x), table.length - 2);
161
+ const f = Math.min(x - i, 1);
162
+ return table[i] * (1 - f) + table[i + 1] * f;
163
+ }
164
+
165
+ return { lEmAtAgeKgm2S };
166
+ }
167
+
168
+ module.exports = { createMoonRecessionHistory, createSolarChannelBudget };
package/src/index.js CHANGED
@@ -125,6 +125,7 @@ export { createPlanetModel } from './planets/model.cjs';
125
125
  // Phase 8.4 — the climate/ΔT machinery, extracted layer by layer.
126
126
  export { createDeltaTCycles } from './deltat/cycles.cjs';
127
127
  export { createDeepTimeLod } from './deltat/deep-time.cjs';
128
+ export { createMoonRecessionHistory, createSolarChannelBudget } from './deltat/recession-history.cjs';
128
129
  export { deltaTEspenakMeeusCanonSeconds } from './deltat/historical.cjs';
129
130
  export { evalClimateL1OrbitalPermil } from './climate/l1-orbital.cjs';
130
131
  // Phase 8.5 — eclipse geometry (single-copy: the browser had no Node twin).
@@ -118,7 +118,12 @@ export const createEpochPrimitives = ({ params: p, alphaAtAgeMa }) => {
118
118
  */
119
119
  const siderealYearSecondsCore = (t) => {
120
120
  if (t === 0) return p.siderealYearJ2000Seconds;
121
- return p.siderealYearJ2000Seconds * (1 - 2 * p.solarMassLossFracPerYear * t * 1e6);
121
+ // Exact Kepler product form (T M⁻² under a·M = const, a(t) = a₀·(1−Δm))
122
+ // — the same Driver-2 law as the planet chains and deltat/deep-time.cjs.
123
+ // MUST stay the bit-exact twin of siderealYearSecondsAtAge there (the
124
+ // transparency gate holds the pair at 84/84 round-trip).
125
+ const dm = p.solarMassLossFracPerYear * t * 1e6;
126
+ return p.siderealYearJ2000Seconds * (1 - dm) * (1 - dm);
122
127
  };
123
128
 
124
129
  /**
package/src/model.js CHANGED
@@ -24,6 +24,7 @@ import { createPhaseMachinery } from './phase/index.cjs';
24
24
  import { createCardinalModel } from './cardinal/index.cjs';
25
25
  import { createDeltaTCycles } from './deltat/cycles.cjs';
26
26
  import { createDeepTimeLod } from './deltat/deep-time.cjs';
27
+ import { createMoonRecessionHistory, createSolarChannelBudget } from './deltat/recession-history.cjs';
27
28
  import { evalClimateL1OrbitalPermil } from './climate/l1-orbital.cjs';
28
29
  import { createMoonEccChannel } from './moon/ecc-channel.cjs';
29
30
  import { createMoonMonthChain } from './moon/month-chain.cjs';
@@ -158,11 +159,38 @@ export function assembleModel(C, F) {
158
159
  gmEarthMoonSystemKm3S2: GM_EARTH_MOON_SYSTEM,
159
160
  });
160
161
 
161
- /** Farhat polynomial on the t_Ma axis. @param {number} tMa @returns {number} */
162
- const moonDistanceMetresAtAge = (tMa) => EPOCH_PARAMS.moonDistanceNowM
163
- * (1 + EPOCH_PARAMS.alpha1PerMa * tMa
164
- + EPOCH_PARAMS.alpha3PerMa3 * tMa * tMa * tMa
165
- + EPOCH_PARAMS.alpha4PerMa4 * tMa * tMa * tMa * tMa);
162
+ // ── Driver 1½: regime-aware recession history + the solar channels ────────
163
+ // The quartic stays bit-identical ≤ jointMa (the Wells/Wu-gated era);
164
+ // beyond, the fitted staircase spline to the Roche crossing, and the
165
+ // ocean-leak/thermal-pump channels make L_EM time-dependent.
166
+ const REGIME = C.deepTime.recessionRegime;
167
+ const recession = createMoonRecessionHistory({
168
+ aMoonNowMetres: EPOCH_PARAMS.moonDistanceNowM,
169
+ alpha1PerMa: EPOCH_PARAMS.alpha1PerMa,
170
+ alpha3PerMa3: EPOCH_PARAMS.alpha3PerMa3,
171
+ alpha4PerMa4: EPOCH_PARAMS.alpha4PerMa4,
172
+ regime: {
173
+ jointMa: REGIME.jointMa,
174
+ knotAgesMa: REGIME.knotAgesMa,
175
+ knotDistancesKm: REGIME.knotDistancesKm,
176
+ genesisMa: REGIME.genesisMa,
177
+ rocheLimitKm: REGIME.rocheLimitKm,
178
+ },
179
+ });
180
+ const moonDistanceMetresAtAge = recession.distanceMetresAtAge;
181
+ const solarBudget = createSolarChannelBudget({
182
+ lTotalJ2000KgM2S: EPOCH_PARAMS.totalAngularMomentumKgM2S,
183
+ mMoonAloneKg: EPOCH_PARAMS.moonMassKg,
184
+ gmEmM3PerS2: EPOCH_PARAMS.gmEarthMoonM3S2,
185
+ eFactorMoon: EPOCH_PARAMS.moonEccentricityFactor,
186
+ beta0: REGIME.solarOceanLeakBeta0,
187
+ pumpStartMa: REGIME.thermalPumpStartMa,
188
+ pumpEndMa: REGIME.thermalPumpEndMa,
189
+ pumpFactor: REGIME.thermalPumpFactor,
190
+ jointMa: REGIME.jointMa,
191
+ genesisMa: REGIME.genesisMa,
192
+ distanceMetresAtAge: moonDistanceMetresAtAge,
193
+ });
166
194
 
167
195
  const DT = F.DT_STACK;
168
196
  const RES = F.DT_RESONATOR;
@@ -222,6 +250,7 @@ export function assembleModel(C, F) {
222
250
  cycleLodSumAt: dtCycleLodCorrectionSum,
223
251
  swingLodAt: (year) => dtCycles.swingLodSecondsAt(year),
224
252
  swingLodRateAt: (year) => dtCycles.swingLodRateAt(year),
253
+ lEmAtAgeKgm2S: solarBudget.lEmAtAgeKgm2S,
225
254
  });
226
255
 
227
256
  // ── Integrated ∫1/H(t)dt phase (R2 α-pin honoured while building) ─────────
@@ -615,6 +644,7 @@ export function assembleModel(C, F) {
615
644
  meanSiderealYearSecondsAtAge: /** @param {number} tMa */ (tMa) => deepLod.siderealYearSecondsAtAge(tMa),
616
645
  meanHAtAge: /** @param {number} tMa */ (tMa) => deepLod.hAtAge(tMa),
617
646
  modulation: /** @param {number} tMa @param {number} s */ (tMa, s) => moonEcc.modulation(tMa, s),
647
+ distanceMetresAtAge: moonDistanceMetresAtAge,
618
648
  },
619
649
  });
620
650
 
@@ -67,10 +67,14 @@
67
67
  * meanSiderealYearSecondsAtAge: (tMa: number) => number,
68
68
  * meanHAtAge: (tMa: number) => (number | null),
69
69
  * modulation: (tMa: number, s: number) => number,
70
+ * distanceMetresAtAge?: (tMa: number) => number,
70
71
  * },
71
72
  * }} deps — fns are the ENGINE'S OWN layer-0/1 evaluators and the shared
72
73
  * eccentricity channel's modulation; constants are the engine's derived
73
74
  * J2000 anchors (both engines derive them from the same JSON).
75
+ * fns.distanceMetresAtAge, when present, OVERRIDES the internal quartic —
76
+ * the Driver-1½ regime-aware history (recession-history.cjs); absent, the
77
+ * internal quartic is the pure twin (bit-identical for t ≤ jointMa).
74
78
  */
75
79
  function createMoonMonthChain({ constants, fns }) {
76
80
  const {
@@ -85,10 +89,11 @@ function createMoonMonthChain({ constants, fns }) {
85
89
 
86
90
  /** Farhat α₁/α₃/α₄ polynomial — Earth–Moon distance in metres at age t_Ma.
87
91
  * @param {number} tMa @returns {number} */
88
- function distanceMetresAtAge(tMa) {
92
+ function quarticMetresAtAge(tMa) {
89
93
  const t = tMa;
90
94
  return aMoonNowMetres * (1 + alpha1PerMa * t + alpha3PerMa3 * t * t * t + alpha4PerMa4 * t * t * t * t);
91
95
  }
96
+ const distanceMetresAtAge = fns.distanceMetresAtAge ?? quarticMetresAtAge;
92
97
 
93
98
  /** @param {number} tMa @returns {number} */
94
99
  function distanceKmAtAge(tMa) {