@cosmosos/mcp-ephemeris 0.1.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 (4) hide show
  1. package/LICENSE +91 -0
  2. package/README.md +107 -0
  3. package/dist/index.js +1808 -0
  4. package/package.json +60 -0
package/dist/index.js ADDED
@@ -0,0 +1,1808 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __createRequire } from 'module';
3
+ const require = __createRequire(import.meta.url);
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
10
+
11
+ // src/index.ts
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
15
+
16
+ // ../ephemeris/src/astrocartography.ts
17
+ var DEG = Math.PI / 180;
18
+ var RAD = 180 / Math.PI;
19
+ function norm360(x) {
20
+ return (x % 360 + 360) % 360;
21
+ }
22
+ function wrap180(x) {
23
+ const v = norm360(x + 180) - 180;
24
+ return v;
25
+ }
26
+ var DEFAULTS = { latMin: -66, latMax: 66, latStep: 1.5 };
27
+ function eclipticToEquatorial(lonDeg, latDeg, oblDeg) {
28
+ const l = lonDeg * DEG;
29
+ const b = latDeg * DEG;
30
+ const e = oblDeg * DEG;
31
+ const sinl = Math.sin(l);
32
+ const ra = Math.atan2(
33
+ sinl * Math.cos(e) - Math.tan(b) * Math.sin(e),
34
+ Math.cos(l)
35
+ );
36
+ const dec = Math.asin(
37
+ Math.sin(b) * Math.cos(e) + Math.cos(b) * Math.sin(e) * sinl
38
+ );
39
+ return { ra: norm360(ra * RAD), dec: dec * RAD };
40
+ }
41
+ function bodyLines(body, gstDeg, opts = {}) {
42
+ const { latMin, latMax, latStep } = { ...DEFAULTS, ...opts };
43
+ const { ra, dec } = body;
44
+ if (!Number.isFinite(ra) || !Number.isFinite(dec)) {
45
+ return { key: body.key, mcLng: NaN, icLng: NaN, asc: [], dsc: [] };
46
+ }
47
+ const mcLng = wrap180(ra - gstDeg);
48
+ const icLng = wrap180(mcLng + 180);
49
+ const asc = [];
50
+ const dsc = [];
51
+ const tanDec = Math.tan(dec * DEG);
52
+ for (let lat = latMax; lat >= latMin - 1e-9; lat -= latStep) {
53
+ const cosH = -Math.tan(lat * DEG) * tanDec;
54
+ if (cosH < -1 || cosH > 1) continue;
55
+ const H = Math.acos(cosH) * RAD;
56
+ asc.push({ lat, lng: wrap180(ra - H - gstDeg) });
57
+ dsc.push({ lat, lng: wrap180(ra + H - gstDeg) });
58
+ }
59
+ return { key: body.key, mcLng, icLng, asc, dsc };
60
+ }
61
+ function computeAcgLines(bodies, gstDeg, opts = {}) {
62
+ return bodies.map((b) => bodyLines(b, gstDeg, opts));
63
+ }
64
+ var MERIDIAN_LAT = 85;
65
+ function bodyPolys(l) {
66
+ return [
67
+ { key: l.key, angle: "MC", pts: [{ x: l.mcLng, y: MERIDIAN_LAT }, { x: l.mcLng, y: -MERIDIAN_LAT }] },
68
+ { key: l.key, angle: "IC", pts: [{ x: l.icLng, y: MERIDIAN_LAT }, { x: l.icLng, y: -MERIDIAN_LAT }] },
69
+ { key: l.key, angle: "AC", pts: l.asc.map((p) => ({ x: p.lng, y: p.lat })) },
70
+ { key: l.key, angle: "DC", pts: l.dsc.map((p) => ({ x: p.lng, y: p.lat })) }
71
+ ];
72
+ }
73
+ function segIntersect(a, b, c, d) {
74
+ const rx = b.x - a.x, ry = b.y - a.y;
75
+ const sx = d.x - c.x, sy = d.y - c.y;
76
+ const den = rx * sy - ry * sx;
77
+ if (!(Math.abs(den) >= 1e-12)) return null;
78
+ const t = ((c.x - a.x) * sy - (c.y - a.y) * sx) / den;
79
+ const u = ((c.x - a.x) * ry - (c.y - a.y) * rx) / den;
80
+ if (!(t >= 0 && t <= 1 && u >= 0 && u <= 1)) return null;
81
+ return { x: a.x + t * rx, y: a.y + t * ry };
82
+ }
83
+ function splitSegments(pts) {
84
+ const segs = [];
85
+ let cur = [];
86
+ let prev = null;
87
+ for (const p of pts) {
88
+ if (prev && Math.abs(p.x - prev.x) > 180) {
89
+ if (cur.length) segs.push(cur);
90
+ cur = [];
91
+ }
92
+ cur.push(p);
93
+ prev = p;
94
+ }
95
+ if (cur.length) segs.push(cur);
96
+ return segs;
97
+ }
98
+ function findParans(lines, dedupDeg = 1.5) {
99
+ const polysByBody = lines.map(bodyPolys);
100
+ const out = [];
101
+ for (let i = 0; i < polysByBody.length; i++) {
102
+ for (let j = i + 1; j < polysByBody.length; j++) {
103
+ for (const la of polysByBody[i]) {
104
+ for (const lb of polysByBody[j]) {
105
+ const segsA = splitSegments(la.pts);
106
+ const segsB = splitSegments(lb.pts);
107
+ for (const sa of segsA) {
108
+ for (let k = 0; k < sa.length - 1; k++) {
109
+ for (const sb of segsB) {
110
+ for (let m = 0; m < sb.length - 1; m++) {
111
+ const x = segIntersect(sa[k], sa[k + 1], sb[m], sb[m + 1]);
112
+ if (!x) continue;
113
+ out.push({
114
+ aKey: la.key,
115
+ bKey: lb.key,
116
+ aAngle: la.angle,
117
+ bAngle: lb.angle,
118
+ lat: x.y,
119
+ lng: x.x
120
+ });
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ const kept = [];
130
+ for (const p of out) {
131
+ const dup = kept.some(
132
+ (q) => q.aKey === p.aKey && q.bKey === p.bKey && q.aAngle === p.aAngle && q.bAngle === p.bAngle && Math.abs(q.lat - p.lat) < dedupDeg && Math.abs(q.lng - p.lng) < dedupDeg
133
+ );
134
+ if (!dup) kept.push(p);
135
+ }
136
+ return kept;
137
+ }
138
+
139
+ // ../ephemeris/src/numerology.ts
140
+ function computeLifePath(localBirthDate) {
141
+ function reduceToDigit(n) {
142
+ while (n > 9) {
143
+ n = String(n).split("").reduce((s, c) => s + Number(c), 0);
144
+ }
145
+ return n;
146
+ }
147
+ function reducePreserveMasters(n) {
148
+ while (n > 9 && n !== 11 && n !== 22 && n !== 33) {
149
+ n = String(n).split("").reduce((s, c) => s + Number(c), 0);
150
+ }
151
+ return n;
152
+ }
153
+ try {
154
+ const m = (localBirthDate || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
155
+ if (!m) return 0;
156
+ const year = Number(m[1]);
157
+ const month = Number(m[2]);
158
+ const day = Number(m[3]);
159
+ return reducePreserveMasters(reduceToDigit(day) + reduceToDigit(month) + reduceToDigit(year));
160
+ } catch {
161
+ return 0;
162
+ }
163
+ }
164
+
165
+ // ../ephemeris/src/engine-core.ts
166
+ var PI = Math.PI;
167
+ var R = PI / 180;
168
+ var D = 180 / PI;
169
+ function n360(x) {
170
+ return (x % 360 + 360) % 360;
171
+ }
172
+ function nutLon(T) {
173
+ const O = (125.04 - 1934.136 * T) * R;
174
+ const L = (280.4665 + 36000.7698 * T) * R;
175
+ const Lp = (218.3165 + 481267.8813 * T) * R;
176
+ return (-17.2 * Math.sin(O) - 1.32 * Math.sin(2 * L) - 0.23 * Math.sin(2 * Lp) + 0.21 * Math.sin(2 * O)) / 3600;
177
+ }
178
+ function obl(T) {
179
+ return 23.439291 - 0.013004 * T - 164e-9 * T * T + 504e-9 * T * T * T;
180
+ }
181
+ function solveKepler(Mdeg, e) {
182
+ const M = Mdeg * R;
183
+ let E = M + e * Math.sin(M);
184
+ for (let i = 0; i < 200; i++) {
185
+ const dE = (M - E + e * Math.sin(E)) / (1 - e * Math.cos(E));
186
+ E += dE;
187
+ if (Math.abs(dE) < 1e-12) break;
188
+ }
189
+ return E;
190
+ }
191
+
192
+ // ../ephemeris/src/vsop.ts
193
+ var PE = {
194
+ earth: { L0: 100.46646, L1: 36000.76983, p0: 102.937, p1: 1198, e0: 0.01671, e1: -42e-6, a: 1, i: 0, Om: 0 },
195
+ mercury: { L0: 252.2503, L1: 149472.675, p0: 77.456, p1: 5719, e0: 0.20563, e1: 204e-7, a: 0.3871, i: 7.005, Om: 48.331 },
196
+ venus: { L0: 181.9798, L1: 58517.816, p0: 131.564, p1: 175, e0: 677e-5, e1: -478e-7, a: 0.72334, i: 3.395, Om: 76.68 },
197
+ mars: { L0: 355.4333, L1: 19140.299, p0: 336.06, p1: 15843, e0: 0.0934, e1: 905e-7, a: 1.52371, i: 1.85, Om: 49.558 },
198
+ jupiter: { L0: 34.3515, L1: 3034.906, p0: 14.331, p1: 7759, e0: 0.04839, e1: 163e-6, a: 5.20289, i: 1.303, Om: 100.464 },
199
+ saturn: { L0: 50.0775, L1: 1222.114, p0: 93.057, p1: 12722, e0: 0.05551, e1: -347e-6, a: 9.53668, i: 2.489, Om: 113.665 },
200
+ uranus: { L0: 314.055, L1: 428.467, p0: 173.005, p1: 4207, e0: 0.0463, e1: -273e-7, a: 19.1892, i: 0.773, Om: 74.006 },
201
+ neptune: { L0: 304.349, L1: 218.486, p0: 48.123, p1: 5766, e0: 899e-5, e1: 64e-7, a: 30.0699, i: 1.77, Om: 131.784 },
202
+ pluto: { L0: 238.929, L1: 145.208, p0: 224.067, p1: 4223, e0: 0.24883, e1: 6e-5, a: 39.4821, i: 17.16, Om: 110.303 },
203
+ chiron: { L0: 209.39, L1: 26098.35, p0: 339.47, p1: 5765, e0: 0.3789, e1: 0, a: 13.648, i: 6.93, Om: 209.21 }
204
+ };
205
+ var OUTER_PLANETS = ["mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto", "chiron"];
206
+ function helioPos3D(key, T) {
207
+ const e = PE[key];
208
+ if (!e) return { x: 0, y: 0, z: 0, lon: 0, r: 0 };
209
+ const L = n360(e.L0 + e.L1 * T);
210
+ const peri = n360(e.p0 + e.p1 * T / 3600);
211
+ const ecc = e.e0 + e.e1 * T;
212
+ const M = n360(L - peri);
213
+ const E = solveKepler(M, ecc);
214
+ const v = 2 * Math.atan2(
215
+ Math.sqrt(1 + ecc) * Math.sin(E / 2),
216
+ Math.sqrt(1 - ecc) * Math.cos(E / 2)
217
+ ) * D;
218
+ const hLon = n360(v + peri);
219
+ const r = e.a * (1 - ecc * Math.cos(E));
220
+ const inc = e.i * R;
221
+ const Om = e.Om * R;
222
+ const lon = hLon * R;
223
+ return {
224
+ x: r * (Math.cos(Om) * Math.cos(lon - Om) - Math.sin(Om) * Math.sin(lon - Om) * Math.cos(inc)),
225
+ y: r * (Math.sin(Om) * Math.cos(lon - Om) + Math.cos(Om) * Math.sin(lon - Om) * Math.cos(inc)),
226
+ z: r * Math.sin(lon - Om) * Math.sin(inc),
227
+ lon: hLon,
228
+ r
229
+ };
230
+ }
231
+ function helioToGeoLon(p, e) {
232
+ const xg = p.x - e.x;
233
+ const yg = p.y - e.y;
234
+ return n360(Math.atan2(yg, xg) * D);
235
+ }
236
+ function helioToGeoLonLat(p, e) {
237
+ const xg = p.x - e.x;
238
+ const yg = p.y - e.y;
239
+ const zg = p.z - e.z;
240
+ return {
241
+ lon: n360(Math.atan2(yg, xg) * D),
242
+ lat: Math.atan2(zg, Math.hypot(xg, yg)) * D
243
+ };
244
+ }
245
+ function isRetrograde(key, JD) {
246
+ if (!(key in PE)) return false;
247
+ const T1 = (JD - 0.5 - 2451545) / 36525;
248
+ const T2 = (JD + 0.5 - 2451545) / 36525;
249
+ const e1 = helioPos3D("earth", T1), e2 = helioPos3D("earth", T2);
250
+ const p1 = helioPos3D(key, T1), p2 = helioPos3D(key, T2);
251
+ const g1 = helioToGeoLon(p1, e1);
252
+ const g2 = helioToGeoLon(p2, e2);
253
+ let diff = g2 - g1;
254
+ if (diff > 180) diff -= 360;
255
+ if (diff < -180) diff += 360;
256
+ return diff < 0;
257
+ }
258
+
259
+ // ../ephemeris/src/solar-lunar.ts
260
+ function sunGeo(T) {
261
+ const L0 = n360(280.46646 + 36000.76983 * T + 3032e-7 * T * T);
262
+ const M = n360(357.52911 + 35999.05029 * T - 1537e-7 * T * T);
263
+ const Mr = M * R;
264
+ const e = 0.016708634 - 42037e-9 * T - 1267e-10 * T * T;
265
+ const C = (1.914602 - 4817e-6 * T - 14e-6 * T * T) * Math.sin(Mr) + (0.019993 - 101e-6 * T) * Math.sin(2 * Mr) + 289e-6 * Math.sin(3 * Mr);
266
+ const lon = n360(L0 + C + nutLon(T) - 5694e-6);
267
+ const r = 1.000001018 * (1 - e * e) / (1 + e * Math.cos(Mr + C * R));
268
+ return { lon, r };
269
+ }
270
+ function moonGeo(T) {
271
+ const Lp = n360(218.3165 + 481267.8813 * T + nutLon(T));
272
+ const D2 = (297.8502 + 445267.1115 * T) * R;
273
+ const M = (357.5291 + 35999.0503 * T) * R;
274
+ const Mp = (134.9634 + 477198.8676 * T) * R;
275
+ const F = (93.272 + 483202.0175 * T) * R;
276
+ return n360(
277
+ Lp + 6.289 * Math.sin(Mp) + 1.274 * Math.sin(2 * D2 - Mp) + 0.658 * Math.sin(2 * D2) + 0.214 * Math.sin(2 * Mp) - 0.186 * Math.sin(M) - 0.114 * Math.sin(2 * F) + 0.059 * Math.sin(2 * D2 - 2 * Mp) + 0.057 * Math.sin(2 * D2 - M - Mp) + 0.053 * Math.sin(2 * D2 + Mp) + 0.046 * Math.sin(2 * D2 - M) - 0.041 * Math.sin(M - Mp) - 0.035 * Math.sin(D2) - 0.03 * Math.sin(M + Mp) + 0.015 * Math.sin(2 * D2 - 2 * F)
278
+ );
279
+ }
280
+ function lunarNode(T) {
281
+ return n360(125.0446 - 1934.1363 * T + 21e-4 * T * T);
282
+ }
283
+ function moonLat(T) {
284
+ const F = (93.2721 + 483202.0175 * T) * R;
285
+ const Mp = (134.9634 + 477198.8676 * T) * R;
286
+ const D2 = (297.8502 + 445267.1115 * T) * R;
287
+ const M = (357.5291 + 35999.0503 * T) * R;
288
+ return 5.128189 * Math.sin(F) + 0.280606 * Math.sin(Mp + F) + 0.277693 * Math.sin(Mp - F) + 0.173238 * Math.sin(2 * D2 - F) + 0.055413 * Math.sin(2 * D2 + F - Mp) + 0.046272 * Math.sin(2 * D2 - F - Mp) + 0.032573 * Math.sin(2 * D2 + F) + 0.017198 * Math.sin(2 * Mp + F) + 9267e-6 * Math.sin(2 * D2 + Mp - F) + 8823e-6 * Math.sin(2 * Mp - F) + 8247e-6 * Math.sin(2 * D2 - M - F);
289
+ }
290
+ var MOON_PHASES_FR = {
291
+ moon_new: { phase: "Nouvelle Lune", description: "Temps des semences et des intentions nouvelles" },
292
+ moon_waxc: { phase: "Premier croissant", description: "Croissance, construction, prise d'\xE9lan" },
293
+ moon_firstq: { phase: "Premier quartier", description: "D\xE9cisions, action, d\xE9passement des obstacles" },
294
+ moon_waxg: { phase: "Gibbeuse croissante", description: "Perfectionnement, ajustements, pers\xE9v\xE9rance" },
295
+ moon_full: { phase: "Pleine Lune", description: "Culmination, r\xE9v\xE9lation, intensit\xE9 \xE9motionnelle" },
296
+ moon_wang: { phase: "Gibbeuse d\xE9croissante", description: "Gratitude, partage, diffusion" },
297
+ moon_lastq: { phase: "Dernier quartier", description: "L\xE2cher-prise, r\xE9visions, ajustements" },
298
+ moon_wanc: { phase: "Dernier croissant", description: "Repos, introspection, purification" }
299
+ };
300
+ function moonPhase(JD) {
301
+ const T = (JD - 2451545) / 36525;
302
+ const sl = sunGeo(T).lon;
303
+ const ml = moonGeo(T);
304
+ const el = n360(ml - sl);
305
+ let k, emoji;
306
+ if (el < 11.25) {
307
+ k = "moon_new";
308
+ emoji = "\u{1F311}";
309
+ } else if (el < 78.75) {
310
+ k = "moon_waxc";
311
+ emoji = "\u{1F312}";
312
+ } else if (el < 101.25) {
313
+ k = "moon_firstq";
314
+ emoji = "\u{1F313}";
315
+ } else if (el < 168.75) {
316
+ k = "moon_waxg";
317
+ emoji = "\u{1F314}";
318
+ } else if (el < 191.25) {
319
+ k = "moon_full";
320
+ emoji = "\u{1F315}";
321
+ } else if (el < 258.75) {
322
+ k = "moon_wang";
323
+ emoji = "\u{1F316}";
324
+ } else if (el < 281.25) {
325
+ k = "moon_lastq";
326
+ emoji = "\u{1F317}";
327
+ } else if (el < 348.75) {
328
+ k = "moon_wanc";
329
+ emoji = "\u{1F318}";
330
+ } else {
331
+ k = "moon_new";
332
+ emoji = "\u{1F311}";
333
+ }
334
+ const info = MOON_PHASES_FR[k];
335
+ const illumination = (1 - Math.cos(el * R)) / 2;
336
+ return {
337
+ key: k,
338
+ phase: info.phase,
339
+ emoji,
340
+ illumination: Math.round(illumination * 100) / 100,
341
+ description: info.description
342
+ };
343
+ }
344
+
345
+ // ../ephemeris/src/houses.ts
346
+ function gmst(JD) {
347
+ const T = (JD - 2451545) / 36525;
348
+ return n360(280.46061837 + 360.98564736629 * (JD - 2451545) + 387933e-9 * T * T - T * T * T / 3871e4);
349
+ }
350
+ function lst(JD, lng) {
351
+ return n360(gmst(JD) + lng);
352
+ }
353
+ function gmstDeg(JD) {
354
+ return gmst(JD);
355
+ }
356
+ function calcASC(RAMC, lat, ob) {
357
+ const t = RAMC * R, l = lat * R, e = ob * R;
358
+ return n360(Math.atan2(Math.cos(t), -(Math.sin(t) * Math.cos(e) + Math.tan(l) * Math.sin(e))) * D);
359
+ }
360
+ function calcMC(RAMC, ob) {
361
+ const t = RAMC * R, e = ob * R;
362
+ return n360(Math.atan2(Math.sin(t), Math.cos(t) * Math.cos(e)) * D);
363
+ }
364
+ function placidusHouses(RAMC, lat, ob) {
365
+ const asc = calcASC(RAMC, lat, ob);
366
+ const mc = calcMC(RAMC, ob);
367
+ const c = new Array(12).fill(0);
368
+ c[0] = asc;
369
+ c[9] = mc;
370
+ c[3] = n360(mc + 180);
371
+ c[6] = n360(asc + 180);
372
+ const ee = ob * R;
373
+ const ll = lat * R;
374
+ function eclOfRA(ra) {
375
+ const r = ra * R;
376
+ return n360(Math.atan2(Math.sin(r), Math.cos(r) * Math.cos(ee)) * D);
377
+ }
378
+ function pCusp(offset, k) {
379
+ let ra = n360(RAMC + offset);
380
+ for (let it = 0; it < 50; it++) {
381
+ const lon = eclOfRA(ra) * R;
382
+ const decl = Math.asin(Math.sin(ee) * Math.sin(lon));
383
+ const x = Math.tan(ll) * Math.tan(decl);
384
+ if (Math.abs(x) >= 1) throw new Error("placidus: cuspide circumpolaire");
385
+ const next = n360(RAMC + offset + k * Math.asin(x) * D);
386
+ const step = Math.abs(n360(next - ra + 180) - 180);
387
+ ra = next;
388
+ if (step < 1e-4) break;
389
+ }
390
+ return eclOfRA(ra);
391
+ }
392
+ try {
393
+ c[10] = pCusp(30, 1 / 3);
394
+ c[11] = pCusp(60, 2 / 3);
395
+ c[1] = pCusp(120, 2 / 3);
396
+ c[2] = pCusp(150, 1 / 3);
397
+ } catch {
398
+ const d1 = n360(asc - mc);
399
+ const d2 = n360(mc + 180 - asc);
400
+ c[10] = n360(mc + d1 / 3);
401
+ c[11] = n360(mc + 2 * d1 / 3);
402
+ c[1] = n360(asc + d2 / 3);
403
+ c[2] = n360(asc + 2 * d2 / 3);
404
+ }
405
+ c[4] = n360(c[10] + 180);
406
+ c[5] = n360(c[11] + 180);
407
+ c[7] = n360(c[1] + 180);
408
+ c[8] = n360(c[2] + 180);
409
+ return c;
410
+ }
411
+ function equalHouses(asc) {
412
+ return Array.from({ length: 12 }, (_, i) => n360(asc + i * 30));
413
+ }
414
+ function wholeSignHouses(asc) {
415
+ const base = Math.floor(asc / 30) * 30;
416
+ return Array.from({ length: 12 }, (_, i) => n360(base + i * 30));
417
+ }
418
+ function kochHouses(RAMC, lat, ob) {
419
+ return placidusHouses(RAMC, lat, ob);
420
+ }
421
+ function calculateHousesByCoords(system, JD, lat, lng) {
422
+ const T = (JD - 2451545) / 36525;
423
+ const ob = obl(T);
424
+ const RA = lst(JD, lng);
425
+ const asc = calcASC(RA, lat, ob);
426
+ const mc = calcMC(RA, ob);
427
+ let cusps;
428
+ switch (system) {
429
+ case "equal":
430
+ cusps = equalHouses(asc);
431
+ break;
432
+ case "whole_sign":
433
+ cusps = wholeSignHouses(asc);
434
+ break;
435
+ case "koch":
436
+ cusps = kochHouses(RA, lat, ob);
437
+ break;
438
+ case "placidus":
439
+ default:
440
+ cusps = placidusHouses(RA, lat, ob);
441
+ }
442
+ return { cusps, asc, mc, vertex: null, system };
443
+ }
444
+ function houseOfLongitude(lon, cusps) {
445
+ for (let i = 0; i < 12; i++) {
446
+ const n = (i + 1) % 12;
447
+ const s = cusps[i], e = cusps[n];
448
+ if (e < s) {
449
+ if (lon >= s || lon < e) return i + 1;
450
+ } else {
451
+ if (lon >= s && lon < e) return i + 1;
452
+ }
453
+ }
454
+ return 1;
455
+ }
456
+
457
+ // ../ephemeris/src/aspects.ts
458
+ var ASPECT_TYPES = [
459
+ { type: "conjunction", nameFr: "Conjonction", angle: 0, orb: 8, tone: "n", symbol: "\u260C" },
460
+ { type: "sextile", nameFr: "Sextile", angle: 60, orb: 6, tone: "h", symbol: "\u26B9" },
461
+ { type: "square", nameFr: "Carr\xE9", angle: 90, orb: 7, tone: "t", symbol: "\u25A1" },
462
+ { type: "trine", nameFr: "Trigone", angle: 120, orb: 8, tone: "h", symbol: "\u25B3" },
463
+ { type: "opposition", nameFr: "Opposition", angle: 180, orb: 8, tone: "t", symbol: "\u260D" },
464
+ { type: "quincunx", nameFr: "Quinconce", angle: 150, orb: 3, tone: "t", symbol: "\u26BB" }
465
+ ];
466
+ var MINOR_ASPECT_TYPES = [
467
+ { type: "semisextile", nameFr: "Semi-sextile", angle: 30, orb: 2, tone: "n", symbol: "\u26BA" },
468
+ { type: "semisquare", nameFr: "Semi-carr\xE9", angle: 45, orb: 2, tone: "t", symbol: "\u2220" },
469
+ { type: "sesquiquadrate", nameFr: "Sesqui-carr\xE9", angle: 135, orb: 2, tone: "t", symbol: "\u26BC" },
470
+ { type: "quintile", nameFr: "Quintile", angle: 72, orb: 2, tone: "n", symbol: "Q" }
471
+ ];
472
+ var NATAL_ASPECT_TYPES = [...ASPECT_TYPES, ...MINOR_ASPECT_TYPES];
473
+ function calculateAspects(pos) {
474
+ const skip = /* @__PURE__ */ new Set([
475
+ "northNode",
476
+ "southNode",
477
+ "fortune",
478
+ "ceres",
479
+ "pallas",
480
+ "juno",
481
+ "vesta",
482
+ "lilithTrue"
483
+ ]);
484
+ const keys = Object.keys(pos).filter((k) => !skip.has(k));
485
+ const out = [];
486
+ for (let i = 0; i < keys.length - 1; i++) {
487
+ for (let j = i + 1; j < keys.length; j++) {
488
+ const k1 = keys[i], k2 = keys[j];
489
+ let d = Math.abs(pos[k1].longitude - pos[k2].longitude);
490
+ if (d > 180) d = 360 - d;
491
+ for (const a of NATAL_ASPECT_TYPES) {
492
+ const isLum = k1 === "sun" || k1 === "moon" || k2 === "sun" || k2 === "moon";
493
+ const maxOrb = a.orb + (isLum ? 2 : 0);
494
+ const o = Math.abs(d - a.angle);
495
+ if (o <= maxOrb) {
496
+ out.push({
497
+ p1: k1,
498
+ p2: k2,
499
+ type: a.type,
500
+ typeFr: a.nameFr,
501
+ angle: a.angle,
502
+ orb: o,
503
+ exact: o < 1,
504
+ symbol: a.symbol,
505
+ tone: a.tone
506
+ });
507
+ }
508
+ }
509
+ }
510
+ }
511
+ return out.sort((a, b) => a.orb - b.orb);
512
+ }
513
+
514
+ // ../ephemeris/src/lots.ts
515
+ function partOfFortune(sunLon, moonLon, asc, isNight) {
516
+ return isNight ? n360(asc + sunLon - moonLon) : n360(asc + moonLon - sunLon);
517
+ }
518
+ function computeHermeticLots(asc, planets, isNight, fortune) {
519
+ const lon = (k) => planets[k]?.longitude ?? 0;
520
+ const lot = (a, b) => n360(isNight ? asc + b - a : asc + a - b);
521
+ const spirit = lot(lon("sun"), lon("moon"));
522
+ return {
523
+ fortune,
524
+ spirit,
525
+ eros: lot(lon("venus"), spirit),
526
+ necessity: lot(fortune, lon("mercury")),
527
+ courage: lot(fortune, lon("mars")),
528
+ victory: lot(lon("jupiter"), spirit),
529
+ nemesis: lot(fortune, lon("saturn"))
530
+ };
531
+ }
532
+
533
+ // ../ephemeris/src/astro-engine.ts
534
+ function allPositions(JD) {
535
+ const T = (JD - 2451545) / 36525;
536
+ const pos = {};
537
+ const sun = sunGeo(T);
538
+ pos["sun"] = {
539
+ key: "sun",
540
+ longitude: sun.lon,
541
+ signIdx: Math.floor(sun.lon / 30),
542
+ degree: sun.lon % 30
543
+ };
544
+ const ml = moonGeo(T);
545
+ pos["moon"] = {
546
+ key: "moon",
547
+ longitude: ml,
548
+ signIdx: Math.floor(ml / 30),
549
+ degree: ml % 30
550
+ };
551
+ const earthH = helioPos3D("earth", T);
552
+ OUTER_PLANETS.forEach((k) => {
553
+ const pH = helioPos3D(k, T);
554
+ const lon = helioToGeoLon(pH, earthH);
555
+ pos[k] = {
556
+ key: k,
557
+ longitude: lon,
558
+ signIdx: Math.floor(lon / 30),
559
+ degree: lon % 30
560
+ };
561
+ });
562
+ const nn = lunarNode(T);
563
+ pos["northNode"] = {
564
+ key: "northNode",
565
+ longitude: nn,
566
+ signIdx: Math.floor(nn / 30),
567
+ degree: nn % 30,
568
+ retrograde: true
569
+ };
570
+ const sn = n360(nn + 180);
571
+ pos["southNode"] = {
572
+ key: "southNode",
573
+ longitude: sn,
574
+ signIdx: Math.floor(sn / 30),
575
+ degree: sn % 30,
576
+ retrograde: true
577
+ };
578
+ return pos;
579
+ }
580
+ var ACG_BODY_KEYS = [
581
+ "sun",
582
+ "moon",
583
+ "mercury",
584
+ "venus",
585
+ "mars",
586
+ "jupiter",
587
+ "saturn",
588
+ "uranus",
589
+ "neptune",
590
+ "pluto"
591
+ ];
592
+ function equatorialPositions(JD) {
593
+ const T = (JD - 2451545) / 36525;
594
+ const eps = obl(T);
595
+ const out = {};
596
+ out["sun"] = eclipticToEquatorial(sunGeo(T).lon, 0, eps);
597
+ out["moon"] = eclipticToEquatorial(moonGeo(T), moonLat(T), eps);
598
+ const earthH = helioPos3D("earth", T);
599
+ for (const k of ["mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"]) {
600
+ const g = helioToGeoLonLat(helioPos3D(k, T), earthH);
601
+ out[k] = eclipticToEquatorial(g.lon, g.lat, eps);
602
+ }
603
+ out["northNode"] = eclipticToEquatorial(lunarNode(T), 0, eps);
604
+ return out;
605
+ }
606
+ function ayanamsa(JD) {
607
+ const T = (JD - 2451545) / 36525;
608
+ const y = 2e3 + T * 100;
609
+ return 22.46 + 1.3748 * (y - 1900) / 100 - 572e-6 * (y - 1900) * (y - 1900) / 1e6;
610
+ }
611
+ function toSidereal(lon, JD) {
612
+ return n360(lon - ayanamsa(JD));
613
+ }
614
+ function computeChartFromJD(JD, latitude, longitude, opts = {}) {
615
+ const zodiac = opts.zodiac ?? "tropical";
616
+ const houseSystem = opts.houseSystem ?? "placidus";
617
+ const T = (JD - 2451545) / 36525;
618
+ let planets = allPositions(JD);
619
+ if (zodiac === "sidereal") {
620
+ const adjusted = {};
621
+ for (const k of Object.keys(planets)) {
622
+ const slon = toSidereal(planets[k].longitude, JD);
623
+ adjusted[k] = {
624
+ ...planets[k],
625
+ longitude: slon,
626
+ signIdx: Math.floor(slon / 30),
627
+ degree: slon % 30
628
+ };
629
+ }
630
+ planets = adjusted;
631
+ }
632
+ const houses = calculateHousesByCoords(houseSystem, JD, latitude, longitude);
633
+ if (zodiac === "sidereal") {
634
+ houses.cusps = houses.cusps.map((c) => toSidereal(c, JD));
635
+ houses.asc = toSidereal(houses.asc, JD);
636
+ houses.mc = toSidereal(houses.mc, JD);
637
+ }
638
+ for (const k of Object.keys(planets)) {
639
+ planets[k].house = houseOfLongitude(planets[k].longitude, houses.cusps);
640
+ }
641
+ const retrogrades = [];
642
+ ["mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"].forEach((k) => {
643
+ const r = isRetrograde(k, JD);
644
+ if (planets[k]) planets[k].retrograde = r;
645
+ if (r) retrogrades.push(k);
646
+ });
647
+ const aspects = calculateAspects(planets);
648
+ const sunLon = planets["sun"].longitude;
649
+ const moonLon = planets["moon"].longitude;
650
+ const sunAbove = (sunLon - houses.asc + 360) % 360 >= 180;
651
+ const pofLon = partOfFortune(sunLon, moonLon, houses.asc, !sunAbove);
652
+ planets["fortune"] = {
653
+ key: "fortune",
654
+ longitude: pofLon,
655
+ signIdx: Math.floor(pofLon / 30),
656
+ degree: pofLon % 30,
657
+ house: houseOfLongitude(pofLon, houses.cusps)
658
+ };
659
+ const phase = moonPhase(JD);
660
+ const numerology = 0;
661
+ const lots = computeHermeticLots(houses.asc, planets, !sunAbove, pofLon);
662
+ return {
663
+ planets,
664
+ houses,
665
+ aspects,
666
+ retrogrades,
667
+ moonPhase: phase,
668
+ numerology,
669
+ zodiac,
670
+ houseSystem,
671
+ JD,
672
+ T,
673
+ ayanamsa: zodiac === "sidereal" ? ayanamsa(JD) : 0,
674
+ lots,
675
+ source: "meeus"
676
+ };
677
+ }
678
+ function computeCurrentSky(lat, lng, opts = {}) {
679
+ const JD = Date.now() / 864e5 + 24405875e-1;
680
+ return computeChartFromJD(JD, lat, lng, opts);
681
+ }
682
+
683
+ // ../ephemeris/src/swiss-engine.ts
684
+ import { createRequire } from "module";
685
+ import { join } from "path";
686
+ var _swe = null;
687
+ var _swissephLoadError = null;
688
+ function loadSwisseph() {
689
+ if (_swe !== null) return _swe !== false;
690
+ try {
691
+ const req = typeof __require === "function" ? __require : createRequire(join(process.cwd(), "package.json"));
692
+ _swe = req("swisseph");
693
+ const path = process.env["SWISSEPH_PATH"] ?? "/usr/local/share/swisseph";
694
+ if (typeof _swe.swe_set_ephe_path === "function") {
695
+ try {
696
+ _swe.swe_set_ephe_path(path);
697
+ } catch {
698
+ }
699
+ }
700
+ return true;
701
+ } catch (err) {
702
+ _swe = false;
703
+ _swissephLoadError = err instanceof Error ? err.message : String(err);
704
+ return false;
705
+ }
706
+ }
707
+ function getSwissephLoadError() {
708
+ return _swissephLoadError;
709
+ }
710
+ function ensureSwissephLoaded() {
711
+ return loadSwisseph();
712
+ }
713
+ var _sidModeSet = false;
714
+ function ayanamsaSwiss(JD) {
715
+ if (!loadSwisseph()) throw new Error("swisseph not loaded");
716
+ if (!_sidModeSet) {
717
+ _swe.swe_set_sid_mode(_swe.SE_SIDM_LAHIRI, 0, 0);
718
+ _sidModeSet = true;
719
+ }
720
+ const r = _swe.swe_get_ayanamsa_ut(JD);
721
+ return typeof r === "number" ? r : r && r.ayanamsa !== void 0 ? r.ayanamsa : r;
722
+ }
723
+ function toSiderealSwiss(lon, JD) {
724
+ return n360(lon - ayanamsaSwiss(JD));
725
+ }
726
+ var _PLANET_IPL = null;
727
+ var FILE_BASED_BODIES = /* @__PURE__ */ new Set([
728
+ "chiron",
729
+ "ceres",
730
+ "pallas",
731
+ "juno",
732
+ "vesta"
733
+ ]);
734
+ var OPTIONAL_BODIES = /* @__PURE__ */ new Set([
735
+ "chiron",
736
+ "ceres",
737
+ "pallas",
738
+ "juno",
739
+ "vesta",
740
+ "lilithTrue"
741
+ ]);
742
+ var _omittedBodiesLogged = /* @__PURE__ */ new Set();
743
+ function logBodyOmission(key, reason) {
744
+ if (_omittedBodiesLogged.has(key)) return;
745
+ _omittedBodiesLogged.add(key);
746
+ console.warn(`[swiss-engine] corps \xAB ${key} \xBB omis (\xE9ph\xE9m\xE9rides .se1 absentes ?) : ${reason}`);
747
+ }
748
+ function getPlanetIpl() {
749
+ if (_PLANET_IPL) return _PLANET_IPL;
750
+ if (!loadSwisseph()) throw new Error("swisseph not loaded");
751
+ _PLANET_IPL = {
752
+ sun: _swe.SE_SUN,
753
+ moon: _swe.SE_MOON,
754
+ mercury: _swe.SE_MERCURY,
755
+ venus: _swe.SE_VENUS,
756
+ mars: _swe.SE_MARS,
757
+ jupiter: _swe.SE_JUPITER,
758
+ saturn: _swe.SE_SATURN,
759
+ uranus: _swe.SE_URANUS,
760
+ neptune: _swe.SE_NEPTUNE,
761
+ pluto: _swe.SE_PLUTO,
762
+ // Nœud lunaire moyen (cohérent avec astro-engine actuel).
763
+ // Pour passer en nœud vrai : SE_TRUE_NODE.
764
+ northNode: _swe.SE_MEAN_NODE,
765
+ // LILITH-V1 : Mean Apogee = "Lilith astrologique" mean (lisse,
766
+ // standard depuis Dane Rudhyar). Alternative `SE_OSCU_APOG = 13`
767
+ // (osculating) — instantanée, oscille beaucoup, peu utilisée en
768
+ // thème natal. Mean = bon défaut.
769
+ lilith: _swe.SE_MEAN_APOG,
770
+ // ASTEROIDS-V1 : Chiron + les 4 astéroïdes « féminins/archétypaux »
771
+ // standards. Codes IPL natifs Swiss Ephemeris (mode fichier requis).
772
+ chiron: _swe.SE_CHIRON,
773
+ ceres: _swe.SE_CERES,
774
+ pallas: _swe.SE_PALLAS,
775
+ juno: _swe.SE_JUNO,
776
+ vesta: _swe.SE_VESTA,
777
+ // Lilith vraie = apogée lunaire osculateur (instantané).
778
+ lilithTrue: _swe.SE_OSCU_APOG
779
+ };
780
+ return _PLANET_IPL;
781
+ }
782
+ var HOUSE_SYSTEM_CHAR = {
783
+ placidus: "P",
784
+ koch: "K",
785
+ equal: "E",
786
+ whole_sign: "W"
787
+ };
788
+ function allPositionsSwiss(JD) {
789
+ if (!loadSwisseph()) throw new Error("swisseph not loaded");
790
+ const ipl = getPlanetIpl();
791
+ const out = {};
792
+ for (const [key, code] of Object.entries(ipl)) {
793
+ const flag = (FILE_BASED_BODIES.has(key) ? _swe.SEFLG_SWIEPH : _swe.SEFLG_MOSEPH) | _swe.SEFLG_SPEED;
794
+ let r;
795
+ try {
796
+ r = _swe.swe_calc_ut(JD, code, flag);
797
+ } catch (err) {
798
+ r = { error: err instanceof Error ? err.message : String(err) };
799
+ }
800
+ const errored = r && typeof r === "object" && "error" in r && r.error;
801
+ const lon = errored ? NaN : n360(r.longitude);
802
+ if (errored || !Number.isFinite(lon)) {
803
+ if (OPTIONAL_BODIES.has(key)) {
804
+ logBodyOmission(key, errored ? String(r.error) : "longitude non finie");
805
+ continue;
806
+ }
807
+ throw new Error(`swe_calc_ut failed for ${key} (ipl=${code}): ${errored ? r.error : "longitude non finie"}`);
808
+ }
809
+ out[key] = {
810
+ key,
811
+ longitude: lon,
812
+ signIdx: Math.floor(lon / 30),
813
+ degree: lon % 30,
814
+ retrograde: typeof r.longitudeSpeed === "number" ? r.longitudeSpeed < 0 : false
815
+ };
816
+ }
817
+ if (out["northNode"]) {
818
+ const sn = n360(out["northNode"].longitude + 180);
819
+ out["southNode"] = {
820
+ key: "southNode",
821
+ longitude: sn,
822
+ signIdx: Math.floor(sn / 30),
823
+ degree: sn % 30,
824
+ // SOUTH-NODE-RETRO-FIX : le Nœud Sud partage l'axe nodal — il a
825
+ // donc le même mouvement que le Nœud Nord. Sans cette ligne il
826
+ // s'affichait « Direct » alors que le Nœud Nord est rétrograde.
827
+ retrograde: out["northNode"].retrograde ?? false
828
+ };
829
+ }
830
+ return out;
831
+ }
832
+ function equatorialPositionsSwiss(JD) {
833
+ if (!loadSwisseph()) throw new Error("swisseph not loaded");
834
+ const flag = _swe.SEFLG_MOSEPH | _swe.SEFLG_EQUATORIAL;
835
+ const ipl = getPlanetIpl();
836
+ const out = {};
837
+ for (const key of ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto", "northNode"]) {
838
+ const code = ipl[key];
839
+ if (typeof code !== "number") continue;
840
+ const r = _swe.swe_calc_ut(JD, code, flag);
841
+ if (r && typeof r === "object" && "error" in r && r.error) {
842
+ throw new Error(`swe_calc_ut (equatorial) failed for ${key}: ${r.error}`);
843
+ }
844
+ const ra = r.rectAscension ?? r.longitude;
845
+ const dec = r.declination ?? r.latitude;
846
+ if (!Number.isFinite(ra) || !Number.isFinite(dec)) {
847
+ throw new Error(
848
+ `swe_calc_ut (equatorial) a renvoy\xE9 des coordonn\xE9es non finies pour ${key} (ra=${ra}, dec=${dec}) \u2014 champs attendus rectAscension/declination.`
849
+ );
850
+ }
851
+ out[key] = { ra: n360(ra), dec };
852
+ }
853
+ return out;
854
+ }
855
+ function calculateHousesSwiss(system, JD, lat, lng) {
856
+ if (!loadSwisseph()) throw new Error("swisseph not loaded");
857
+ const hsys = HOUSE_SYSTEM_CHAR[system] ?? "P";
858
+ const r = _swe.swe_houses(JD, lat, lng, hsys);
859
+ if (r && typeof r === "object" && "error" in r && r.error) {
860
+ throw new Error(`swe_houses failed: ${r.error}`);
861
+ }
862
+ const cusps = r.house.map(n360);
863
+ const vertex = typeof r.vertex === "number" ? n360(r.vertex) : null;
864
+ return {
865
+ cusps,
866
+ asc: n360(r.ascendant),
867
+ mc: n360(r.mc),
868
+ vertex,
869
+ system
870
+ };
871
+ }
872
+ function moonPhaseFromLongitudes(sunLon, moonLon) {
873
+ const el = n360(moonLon - sunLon);
874
+ let key, emoji;
875
+ if (el < 11.25) {
876
+ key = "moon_new";
877
+ emoji = "\u{1F311}";
878
+ } else if (el < 78.75) {
879
+ key = "moon_waxc";
880
+ emoji = "\u{1F312}";
881
+ } else if (el < 101.25) {
882
+ key = "moon_firstq";
883
+ emoji = "\u{1F313}";
884
+ } else if (el < 168.75) {
885
+ key = "moon_waxg";
886
+ emoji = "\u{1F314}";
887
+ } else if (el < 191.25) {
888
+ key = "moon_full";
889
+ emoji = "\u{1F315}";
890
+ } else if (el < 258.75) {
891
+ key = "moon_wang";
892
+ emoji = "\u{1F316}";
893
+ } else if (el < 281.25) {
894
+ key = "moon_lastq";
895
+ emoji = "\u{1F317}";
896
+ } else if (el < 348.75) {
897
+ key = "moon_wanc";
898
+ emoji = "\u{1F318}";
899
+ } else {
900
+ key = "moon_new";
901
+ emoji = "\u{1F311}";
902
+ }
903
+ const FR = {
904
+ moon_new: { phase: "Nouvelle Lune", description: "Temps des semences et des intentions nouvelles" },
905
+ moon_waxc: { phase: "Premier croissant", description: "Croissance, construction, prise d'\xE9lan" },
906
+ moon_firstq: { phase: "Premier quartier", description: "D\xE9cisions, action, d\xE9passement des obstacles" },
907
+ moon_waxg: { phase: "Gibbeuse croissante", description: "Perfectionnement, ajustements, pers\xE9v\xE9rance" },
908
+ moon_full: { phase: "Pleine Lune", description: "Culmination, r\xE9v\xE9lation, intensit\xE9 \xE9motionnelle" },
909
+ moon_wang: { phase: "Gibbeuse d\xE9croissante", description: "Gratitude, partage, diffusion" },
910
+ moon_lastq: { phase: "Dernier quartier", description: "L\xE2cher-prise, r\xE9visions, ajustements" },
911
+ moon_wanc: { phase: "Dernier croissant", description: "Repos, introspection, purification" }
912
+ };
913
+ const info = FR[key];
914
+ const illumination = (1 - Math.cos(el * Math.PI / 180)) / 2;
915
+ return {
916
+ key,
917
+ phase: info.phase,
918
+ emoji,
919
+ illumination: Math.round(illumination * 100) / 100,
920
+ description: info.description
921
+ };
922
+ }
923
+ function computeChartFromJDSwiss(JD, latitude, longitude, opts = {}) {
924
+ const zodiac = opts.zodiac ?? "tropical";
925
+ const houseSystem = opts.houseSystem ?? "placidus";
926
+ const T = (JD - 2451545) / 36525;
927
+ let planets = allPositionsSwiss(JD);
928
+ if (zodiac === "sidereal") {
929
+ const adjusted = {};
930
+ for (const k of Object.keys(planets)) {
931
+ const slon = toSiderealSwiss(planets[k].longitude, JD);
932
+ adjusted[k] = {
933
+ ...planets[k],
934
+ longitude: slon,
935
+ signIdx: Math.floor(slon / 30),
936
+ degree: slon % 30
937
+ };
938
+ }
939
+ planets = adjusted;
940
+ }
941
+ const houses = calculateHousesSwiss(houseSystem, JD, latitude, longitude);
942
+ if (zodiac === "sidereal") {
943
+ houses.cusps = houses.cusps.map((c) => toSiderealSwiss(c, JD));
944
+ houses.asc = toSiderealSwiss(houses.asc, JD);
945
+ houses.mc = toSiderealSwiss(houses.mc, JD);
946
+ if (houses.vertex != null) houses.vertex = toSiderealSwiss(houses.vertex, JD);
947
+ }
948
+ for (const k of Object.keys(planets)) {
949
+ planets[k].house = houseOfLongitude(planets[k].longitude, houses.cusps);
950
+ }
951
+ const retrogrades = [];
952
+ for (const k of ["mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto", "chiron", "ceres", "pallas", "juno", "vesta"]) {
953
+ if (planets[k]?.retrograde) retrogrades.push(k);
954
+ }
955
+ const aspects = calculateAspects(planets);
956
+ const sunLon = planets["sun"].longitude;
957
+ const moonLon = planets["moon"].longitude;
958
+ const sunAbove = (sunLon - houses.asc + 360) % 360 >= 180;
959
+ const pofLon = partOfFortune(sunLon, moonLon, houses.asc, !sunAbove);
960
+ planets["fortune"] = {
961
+ key: "fortune",
962
+ longitude: pofLon,
963
+ signIdx: Math.floor(pofLon / 30),
964
+ degree: pofLon % 30,
965
+ house: houseOfLongitude(pofLon, houses.cusps)
966
+ };
967
+ const lots = computeHermeticLots(houses.asc, planets, !sunAbove, pofLon);
968
+ const phase = moonPhaseFromLongitudes(sunLon, moonLon);
969
+ void moonPhase;
970
+ return {
971
+ planets,
972
+ houses,
973
+ aspects,
974
+ retrogrades,
975
+ moonPhase: phase,
976
+ numerology: 0,
977
+ // dérivée plus tard à partir de localBirthDate
978
+ zodiac,
979
+ houseSystem,
980
+ JD,
981
+ T,
982
+ ayanamsa: zodiac === "sidereal" ? ayanamsaSwiss(JD) : 0,
983
+ lots,
984
+ // C2-FIX : ChartResult.source est désormais "meeus" | "swiss" — on
985
+ // rapporte le vrai moteur. getEngineDiagnostic() reste la source
986
+ // détaillée pour /admin/ephemeris/health.
987
+ source: "swiss"
988
+ };
989
+ }
990
+ function computeCurrentSkySwiss(lat, lng, opts = {}) {
991
+ const JD = Date.now() / 864e5 + 24405875e-1;
992
+ return computeChartFromJDSwiss(JD, lat, lng, opts);
993
+ }
994
+ function moonPhaseSwiss(JD) {
995
+ if (!loadSwisseph()) throw new Error("swisseph not loaded");
996
+ const ipl = getPlanetIpl();
997
+ const sun = _swe.swe_calc_ut(JD, ipl.sun, _swe.SEFLG_MOSEPH);
998
+ const moon = _swe.swe_calc_ut(JD, ipl.moon, _swe.SEFLG_MOSEPH);
999
+ return moonPhaseFromLongitudes(n360(sun.longitude), n360(moon.longitude));
1000
+ }
1001
+
1002
+ // ../ephemeris/src/engine-router.ts
1003
+ var _activeEngine = null;
1004
+ var _resolutionLog = null;
1005
+ function resolveEngine() {
1006
+ if (_activeEngine) return _activeEngine;
1007
+ const requested = (process.env["ASTRO_ENGINE"] ?? "swisseph").toLowerCase().trim();
1008
+ if (requested === "astracore") {
1009
+ _activeEngine = "astracore";
1010
+ _resolutionLog = "ASTRO_ENGINE=astracore (explicite)";
1011
+ return _activeEngine;
1012
+ }
1013
+ if (ensureSwissephLoaded()) {
1014
+ _activeEngine = "swisseph";
1015
+ _resolutionLog = `ASTRO_ENGINE=${requested} \u2192 swisseph charg\xE9 OK`;
1016
+ return _activeEngine;
1017
+ }
1018
+ const err = getSwissephLoadError();
1019
+ const isProd = (process.env["NODE_ENV"] ?? "").toLowerCase() === "production";
1020
+ const allowFallback = (process.env["ASTRO_ALLOW_FALLBACK"] ?? "").toLowerCase() === "true";
1021
+ if (isProd && !allowFallback) {
1022
+ const msg = `[ephemeris/router] FATAL: ASTRO_ENGINE=${requested} mais swisseph indisponible (${err ?? "raison inconnue"}). Le fallback AstraCore n'applique pas \u0394T et servirait des th\xE8mes faux. Refus du fallback silencieux en production. Corrige le build/volume swisseph, ou pose ASTRO_ALLOW_FALLBACK=true pour accepter explicitement AstraCore.`;
1023
+ console.error(msg);
1024
+ throw new Error(msg);
1025
+ }
1026
+ _activeEngine = "astracore";
1027
+ _resolutionLog = `ASTRO_ENGINE=${requested} demand\xE9 mais swisseph indisponible (${err ?? "raison inconnue"}) \u2014 fallback sur astracore` + (allowFallback ? " (ASTRO_ALLOW_FALLBACK=true)" : " (hors production)");
1028
+ console.warn(`[ephemeris/router] ${_resolutionLog}`);
1029
+ return _activeEngine;
1030
+ }
1031
+ function getActiveEngine() {
1032
+ return resolveEngine();
1033
+ }
1034
+ function computeChartFromJD2(JD, latitude, longitude, opts = {}) {
1035
+ if (resolveEngine() === "swisseph") {
1036
+ return computeChartFromJDSwiss(JD, latitude, longitude, opts);
1037
+ }
1038
+ return computeChartFromJD(JD, latitude, longitude, opts);
1039
+ }
1040
+ function computeCurrentSky2(lat, lng, opts = {}) {
1041
+ if (resolveEngine() === "swisseph") {
1042
+ return computeCurrentSkySwiss(lat, lng, opts);
1043
+ }
1044
+ return computeCurrentSky(lat, lng, opts);
1045
+ }
1046
+ function allPositions2(JD) {
1047
+ return resolveEngine() === "swisseph" ? allPositionsSwiss(JD) : allPositions(JD);
1048
+ }
1049
+ function moonPhase2(JD) {
1050
+ return resolveEngine() === "swisseph" ? moonPhaseSwiss(JD) : moonPhase(JD);
1051
+ }
1052
+ function equatorialPositions2(JD) {
1053
+ return resolveEngine() === "swisseph" ? equatorialPositionsSwiss(JD) : equatorialPositions(JD);
1054
+ }
1055
+ function computeAstrocartography(JD, opts = {}) {
1056
+ const eq = equatorialPositions2(JD);
1057
+ const gst = gmstDeg(JD);
1058
+ const allow = new Set(opts.bodyKeys ?? ACG_BODY_KEYS);
1059
+ const bodies = Object.entries(eq).filter(([key]) => allow.has(key)).map(([key, c]) => ({ key, ra: c.ra, dec: c.dec }));
1060
+ const lineOpts = opts.latStep ? { latStep: opts.latStep } : {};
1061
+ const lines = computeAcgLines(bodies, gst, lineOpts);
1062
+ const parans = findParans(lines);
1063
+ return { jd: JD, gst, bodies, lines, parans };
1064
+ }
1065
+
1066
+ // ../ephemeris/src/types.ts
1067
+ var CityNotFoundError = class extends Error {
1068
+ constructor(query, suggestions = []) {
1069
+ super(
1070
+ suggestions.length > 0 ? `City "${query}" not found. Did you mean: ${suggestions.join(", ")}?` : `City "${query}" not found.`
1071
+ );
1072
+ this.query = query;
1073
+ this.suggestions = suggestions;
1074
+ this.name = "CityNotFoundError";
1075
+ }
1076
+ query;
1077
+ suggestions;
1078
+ };
1079
+
1080
+ // ../ephemeris/src/time-utc.service.ts
1081
+ import { DateTime } from "luxon";
1082
+ var TimezoneError = class extends Error {
1083
+ constructor(message, code, details) {
1084
+ super(message);
1085
+ this.code = code;
1086
+ this.details = details;
1087
+ this.name = "TimezoneError";
1088
+ }
1089
+ code;
1090
+ details;
1091
+ };
1092
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1093
+ var TIME_RE = /^\d{2}:\d{2}(:\d{2})?$/;
1094
+ function assertValidDate(date) {
1095
+ if (!DATE_RE.test(date)) {
1096
+ throw new TimezoneError(
1097
+ `Birth date must be YYYY-MM-DD, got "${date}"`,
1098
+ "INVALID_DATE",
1099
+ { date }
1100
+ );
1101
+ }
1102
+ }
1103
+ function assertValidTime(time) {
1104
+ if (!TIME_RE.test(time)) {
1105
+ throw new TimezoneError(
1106
+ `Birth time must be HH:MM or HH:MM:SS, got "${time}"`,
1107
+ "INVALID_TIME",
1108
+ { time }
1109
+ );
1110
+ }
1111
+ }
1112
+ function assertValidIanaTz(ianaTz) {
1113
+ if (NUMERIC_OFFSET_RE.test(ianaTz)) {
1114
+ throw new TimezoneError(
1115
+ `Numeric offset "${ianaTz}" not accepted \u2014 use an IANA timezone like "Europe/Paris" instead. Fixed offsets lose DST history.`,
1116
+ "INVALID_IANA_TZ",
1117
+ { ianaTz }
1118
+ );
1119
+ }
1120
+ if (!DateTime.local().setZone(ianaTz).isValid) {
1121
+ throw new TimezoneError(
1122
+ `Unknown IANA timezone "${ianaTz}". Expected e.g. "Europe/Paris".`,
1123
+ "INVALID_IANA_TZ",
1124
+ { ianaTz }
1125
+ );
1126
+ }
1127
+ }
1128
+ var NUMERIC_OFFSET_RE = /^(UTC|GMT|UT)?[+-]\d{1,2}(:?\d{2})?$/i;
1129
+ function localToUTC(localDate, localTime, ianaTz, opts = {}) {
1130
+ assertValidDate(localDate);
1131
+ assertValidTime(localTime);
1132
+ assertValidIanaTz(ianaTz);
1133
+ const onAmbiguous = opts.onAmbiguous ?? "earliest";
1134
+ const onNonExistent = opts.onNonExistent ?? "shiftLater";
1135
+ const [hStr, mStr, sStr] = localTime.split(":");
1136
+ const hour = Number(hStr);
1137
+ const minute = Number(mStr);
1138
+ const second = sStr ? Number(sStr) : 0;
1139
+ const [yStr, moStr, dStr] = localDate.split("-");
1140
+ const year = Number(yStr);
1141
+ const month = Number(moStr);
1142
+ const day = Number(dStr);
1143
+ const dt = DateTime.fromObject(
1144
+ { year, month, day, hour, minute, second },
1145
+ { zone: ianaTz }
1146
+ );
1147
+ if (!dt.isValid) {
1148
+ throw new TimezoneError(
1149
+ `Luxon rejected ${localDate} ${localTime} @ ${ianaTz}: ${dt.invalidReason} \u2014 ${dt.invalidExplanation}`,
1150
+ "INVALID_DATE",
1151
+ { localDate, localTime, ianaTz }
1152
+ );
1153
+ }
1154
+ if (typeof opts.longitude === "number" && !Number.isInteger(dt.offset)) {
1155
+ const lmtOffsetMinutes = opts.longitude * 4;
1156
+ const utc = DateTime.fromObject({ year, month, day, hour, minute, second }, { zone: "utc" }).minus({ milliseconds: Math.round(lmtOffsetMinutes * 6e4) });
1157
+ const utcMs = utc.toMillis();
1158
+ return {
1159
+ jdUT: utcMs / 864e5 + 24405875e-1,
1160
+ offsetMinutes: lmtOffsetMinutes,
1161
+ utcISO: utc.toUTC().toISO() ?? "",
1162
+ resolution: "valid"
1163
+ // aucune bascule DST possible en ère LMT
1164
+ };
1165
+ }
1166
+ let resolution = "valid";
1167
+ if (dt.hour !== hour || dt.minute !== minute) {
1168
+ if (onNonExistent === "throw") {
1169
+ throw new TimezoneError(
1170
+ `Time ${localTime} on ${localDate} does not exist in ${ianaTz} (DST spring-forward).`,
1171
+ "NONEXISTENT_TIME",
1172
+ { localDate, localTime, ianaTz, shiftedTo: dt.toISO() }
1173
+ );
1174
+ }
1175
+ resolution = "nonexistent";
1176
+ }
1177
+ if (resolution === "valid") {
1178
+ const before = dt.minus({ minutes: 30 });
1179
+ const after = dt.plus({ minutes: 30 });
1180
+ if (before.offset !== dt.offset || after.offset !== dt.offset) {
1181
+ const alt = dt.plus({ hours: 1 });
1182
+ const altAsWall = DateTime.fromObject(
1183
+ { year: alt.year, month: alt.month, day: alt.day, hour: alt.hour, minute: alt.minute, second: alt.second },
1184
+ { zone: ianaTz }
1185
+ );
1186
+ if (altAsWall.toMillis() !== alt.toMillis()) {
1187
+ if (onAmbiguous === "throw") {
1188
+ throw new TimezoneError(
1189
+ `Time ${localTime} on ${localDate} is ambiguous in ${ianaTz} (DST fall-back). Ask the user which occurrence.`,
1190
+ "AMBIGUOUS_TIME",
1191
+ { localDate, localTime, ianaTz }
1192
+ );
1193
+ }
1194
+ if (onAmbiguous === "latest") {
1195
+ const late = dt.plus({ hours: 1 }).setZone(ianaTz, { keepLocalTime: false });
1196
+ return buildResult(late, "ambiguous");
1197
+ }
1198
+ resolution = "ambiguous";
1199
+ }
1200
+ }
1201
+ }
1202
+ return buildResult(dt, resolution);
1203
+ }
1204
+ function buildResult(dt, resolution) {
1205
+ const utcMs = dt.toUTC().toMillis();
1206
+ const jdUT = utcMs / 864e5 + 24405875e-1;
1207
+ return {
1208
+ jdUT,
1209
+ offsetMinutes: dt.offset,
1210
+ utcISO: dt.toUTC().toISO() ?? "",
1211
+ resolution
1212
+ };
1213
+ }
1214
+
1215
+ // ../ephemeris/src/service.ts
1216
+ import { createHash } from "crypto";
1217
+ var _redis = null;
1218
+ async function getRedis() {
1219
+ if (_redis !== null) return _redis;
1220
+ if (process.env["EPHEMERIS_DISABLE_REDIS"] === "1") {
1221
+ _redis = false;
1222
+ return null;
1223
+ }
1224
+ try {
1225
+ const { createClient } = await import("redis");
1226
+ const client = createClient({
1227
+ url: process.env["REDIS_URL"] ?? "redis://redis:6379",
1228
+ socket: { connectTimeout: 500, reconnectStrategy: false }
1229
+ });
1230
+ client.on("error", () => {
1231
+ });
1232
+ await client.connect();
1233
+ _redis = client;
1234
+ return client;
1235
+ } catch {
1236
+ _redis = false;
1237
+ return null;
1238
+ }
1239
+ }
1240
+ async function cacheGet(key) {
1241
+ try {
1242
+ const r = await getRedis();
1243
+ if (!r) return null;
1244
+ const v = await r.get(key);
1245
+ return v ? JSON.parse(v) : null;
1246
+ } catch {
1247
+ return null;
1248
+ }
1249
+ }
1250
+ async function cacheSet(key, data, ttl = 86400) {
1251
+ try {
1252
+ const r = await getRedis();
1253
+ if (!r) return;
1254
+ await r.setEx(key, ttl, JSON.stringify(data));
1255
+ } catch {
1256
+ }
1257
+ }
1258
+ function enrich(result, meta) {
1259
+ const houses = result.houses.cusps.map((lon, i) => ({
1260
+ number: i + 1,
1261
+ longitude: lon,
1262
+ signIdx: Math.floor(lon / 30)
1263
+ }));
1264
+ const aspects = result.aspects.map((a) => ({
1265
+ planet1: a.p1,
1266
+ planet2: a.p2,
1267
+ p1: a.p1,
1268
+ p2: a.p2,
1269
+ type: a.type,
1270
+ typeFr: a.typeFr,
1271
+ orb: a.orb,
1272
+ angle: a.angle,
1273
+ exact: a.exact,
1274
+ symbol: a.symbol,
1275
+ tone: a.tone
1276
+ }));
1277
+ return {
1278
+ planets: result.planets,
1279
+ houses,
1280
+ asc: result.houses.asc,
1281
+ mc: result.houses.mc,
1282
+ vertex: result.houses.vertex,
1283
+ // VERTEX-V1
1284
+ lots: result.lots,
1285
+ // POINTS-ARABES-V1
1286
+ aspects,
1287
+ retrogrades: result.retrogrades,
1288
+ moonPhase: result.moonPhase,
1289
+ numerology: meta.localBirthDate ? computeLifePath(meta.localBirthDate) : result.numerology,
1290
+ // STAB-PRE-5-V1
1291
+ source: result.source,
1292
+ zodiac: result.zodiac,
1293
+ houseSystem: result.houseSystem,
1294
+ JD: result.JD,
1295
+ meta
1296
+ };
1297
+ }
1298
+ function chartCacheKey(args) {
1299
+ const payload = JSON.stringify({
1300
+ d: args.localBirthDate,
1301
+ t: args.localBirthTime,
1302
+ z: args.ianaTz,
1303
+ // On quantifie lat/lng à 4 décimales (~10 m) pour absorber
1304
+ // les micro-variations de saisie sans invalider tout le temps.
1305
+ la: Number(args.latitude.toFixed(4)),
1306
+ lo: Number(args.longitude.toFixed(4)),
1307
+ zo: args.zodiac,
1308
+ hs: args.houseSystem,
1309
+ tk: args.birthTimeKnown,
1310
+ // C6-FIX : le moteur actif entre dans la clé — un repli gracieux
1311
+ // swisseph↔astracore ne doit pas resservir un thème de l'autre moteur.
1312
+ e: getActiveEngine()
1313
+ });
1314
+ const hash = createHash("sha256").update(payload).digest("hex").slice(0, 16);
1315
+ return `chart:v7:${args.natalId}:${hash}`;
1316
+ }
1317
+ var EphemerisService = class {
1318
+ /**
1319
+ * Calcule un thème natal à partir d'une saisie locale + IANA tz.
1320
+ *
1321
+ * ✦ Nouvelle API recommandée. La conversion vers UTC et le JD
1322
+ * se font ici, une seule fois, côté serveur, sans convention
1323
+ * implicite.
1324
+ */
1325
+ async calculateNatalChart(input) {
1326
+ const zodiac = input.zodiac ?? "tropical";
1327
+ const houseSystem = input.houseSystem ?? "placidus";
1328
+ const conv = localToUTC(
1329
+ input.localBirthDate,
1330
+ input.localBirthTime,
1331
+ input.ianaTz,
1332
+ {
1333
+ onAmbiguous: input.onAmbiguous ?? "earliest",
1334
+ onNonExistent: input.onNonExistent ?? "shiftLater",
1335
+ longitude: input.longitude
1336
+ }
1337
+ );
1338
+ const cacheKey = chartCacheKey({
1339
+ natalId: input.natalId,
1340
+ localBirthDate: input.localBirthDate,
1341
+ localBirthTime: input.localBirthTime,
1342
+ ianaTz: input.ianaTz,
1343
+ latitude: input.latitude,
1344
+ longitude: input.longitude,
1345
+ zodiac,
1346
+ houseSystem,
1347
+ birthTimeKnown: input.birthTimeKnown
1348
+ });
1349
+ const cached = await cacheGet(cacheKey);
1350
+ if (cached) return cached;
1351
+ const result = computeChartFromJD2(
1352
+ conv.jdUT,
1353
+ input.latitude,
1354
+ input.longitude,
1355
+ { zodiac, houseSystem }
1356
+ );
1357
+ const meta = {
1358
+ localBirthDate: input.localBirthDate,
1359
+ localBirthTime: input.localBirthTime,
1360
+ ianaTz: input.ianaTz,
1361
+ offsetMinutes: conv.offsetMinutes,
1362
+ resolution: conv.resolution,
1363
+ birthTimeKnown: input.birthTimeKnown,
1364
+ utcISO: conv.utcISO
1365
+ };
1366
+ const enriched = enrich(result, meta);
1367
+ await cacheSet(cacheKey, enriched, 7 * 24 * 3600);
1368
+ return enriched;
1369
+ }
1370
+ // ──────────────────────────────────────────────────────
1371
+ // EPHEMERIS-DEEP-CONSOLIDATION-V1
1372
+ // City resolver injectable : l'app fournit le lookup ville→coords
1373
+ // (typiquement via la table Postgres `cities` GeoNames). Le
1374
+ // package ephemeris est ainsi 100% indépendant de toute liste
1375
+ // hardcodée.
1376
+ // ──────────────────────────────────────────────────────
1377
+ _cityResolver = null;
1378
+ /**
1379
+ * Injecte le resolver de ville. À appeler une fois au boot de
1380
+ * l'application (ex: apps/api/src/index.ts).
1381
+ *
1382
+ * Le resolver peut être sync ou async. Doit retourner null si
1383
+ * la ville n'est pas trouvée — le service convertit ce null en
1384
+ * `CityNotFoundError` pour le caller.
1385
+ */
1386
+ setCityResolver(resolver) {
1387
+ this._cityResolver = resolver;
1388
+ }
1389
+ /**
1390
+ * Version convenience : résout le nom de ville en interne via
1391
+ * le resolver injecté au boot.
1392
+ *
1393
+ * Lève `CityNotFoundError` si :
1394
+ * - aucun resolver n'a été injecté (mauvaise config app)
1395
+ * - le resolver retourne null (ville inconnue)
1396
+ */
1397
+ async calculateFromCityName(args) {
1398
+ const { cityName, ...rest } = args;
1399
+ if (!this._cityResolver) {
1400
+ throw new CityNotFoundError(
1401
+ cityName,
1402
+ ["(no city resolver injected \u2014 check ephemerisService.setCityResolver() at app boot)"]
1403
+ );
1404
+ }
1405
+ const city = await Promise.resolve(this._cityResolver(cityName));
1406
+ if (!city) {
1407
+ throw new CityNotFoundError(cityName, []);
1408
+ }
1409
+ return this.calculateNatalChart({
1410
+ ...rest,
1411
+ ianaTz: city.ianaTz,
1412
+ latitude: city.lat,
1413
+ longitude: city.lng
1414
+ });
1415
+ }
1416
+ /** Ciel du moment présent (transits) pour une position. */
1417
+ async getCurrentSky(latitude, longitude, opts = {}) {
1418
+ const result = computeCurrentSky2(latitude, longitude, opts);
1419
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1420
+ return enrich(result, {
1421
+ localBirthDate: nowIso.slice(0, 10),
1422
+ localBirthTime: nowIso.slice(11, 16),
1423
+ ianaTz: "UTC",
1424
+ offsetMinutes: 0,
1425
+ resolution: "valid",
1426
+ birthTimeKnown: true,
1427
+ utcISO: nowIso
1428
+ });
1429
+ }
1430
+ // ──────────────────────────────────────────────────────
1431
+ // LEGACY — conservé pour compat pendant la migration
1432
+ // ──────────────────────────────────────────────────────
1433
+ /**
1434
+ * @deprecated Utilisez `calculateNatalChart({ localBirthDate, localBirthTime, ianaTz, … })`.
1435
+ *
1436
+ * Ancienne signature : `birthTimeUT` en heures décimales UT.
1437
+ * On la garde pour ne pas casser les appelants qui n'ont pas
1438
+ * encore migré, mais elle est marquée deprecated.
1439
+ */
1440
+ async calculateNatalChartLegacy(natalId, birthDate, birthTimeUT, latitude, longitude, opts = {}) {
1441
+ console.warn(
1442
+ `[ephemeris] calculateNatalChartLegacy called for natalId=${natalId}. Migrate to calculateNatalChart({ localBirthDate, localBirthTime, ianaTz, \u2026 }).`
1443
+ );
1444
+ const hh = Math.floor(birthTimeUT);
1445
+ const mm = Math.floor((birthTimeUT - hh) * 60);
1446
+ const localBirthTime = `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
1447
+ return this.calculateNatalChart({
1448
+ natalId,
1449
+ localBirthDate: birthDate,
1450
+ localBirthTime,
1451
+ ianaTz: "UTC",
1452
+ // on impose UTC puisque l'ancienne signature passait déjà en UT
1453
+ latitude,
1454
+ longitude,
1455
+ birthTimeKnown: true,
1456
+ // on ne sait pas, on suppose oui par défaut
1457
+ zodiac: opts.zodiac,
1458
+ houseSystem: opts.houseSystem
1459
+ });
1460
+ }
1461
+ /**
1462
+ * @deprecated EPHEMERIS-DEEP-CONSOLIDATION-V1 — utiliser
1463
+ * directement le resolver via setCityResolver() ou appeler
1464
+ * calculateFromCityName() qui l'utilise en interne.
1465
+ * Conservé pour rétro-compat ; bascule async sur le resolver injecté.
1466
+ */
1467
+ async getCityCoords(name) {
1468
+ if (!this._cityResolver) {
1469
+ throw new CityNotFoundError(
1470
+ name,
1471
+ ["(no city resolver injected \u2014 check ephemerisService.setCityResolver() at app boot)"]
1472
+ );
1473
+ }
1474
+ const city = await Promise.resolve(this._cityResolver(name));
1475
+ if (!city) throw new CityNotFoundError(name, []);
1476
+ return city;
1477
+ }
1478
+ };
1479
+ var ephemerisService = new EphemerisService();
1480
+
1481
+ // src/index.ts
1482
+ process.env["EPHEMERIS_DISABLE_REDIS"] ??= "1";
1483
+ process.env["ASTRO_ALLOW_FALLBACK"] ??= "true";
1484
+ var SIGNS_FR = [
1485
+ "B\xE9lier",
1486
+ "Taureau",
1487
+ "G\xE9meaux",
1488
+ "Cancer",
1489
+ "Lion",
1490
+ "Vierge",
1491
+ "Balance",
1492
+ "Scorpion",
1493
+ "Sagittaire",
1494
+ "Capricorne",
1495
+ "Verseau",
1496
+ "Poissons"
1497
+ ];
1498
+ function jdFromDate(d) {
1499
+ return d.getTime() / 864e5 + 24405875e-1;
1500
+ }
1501
+ function parseUtc(datetime) {
1502
+ const d = new Date(datetime);
1503
+ if (Number.isNaN(d.getTime())) {
1504
+ throw new Error(`datetime invalide : "${datetime}" (attendu ISO 8601, ex. 2026-07-18T14:00:00Z)`);
1505
+ }
1506
+ return d;
1507
+ }
1508
+ function formatPlanets(positions) {
1509
+ return Object.values(positions).map((p) => ({
1510
+ corps: p.key,
1511
+ longitude: Number(p.longitude?.toFixed(4)),
1512
+ signe: SIGNS_FR[p.signIdx] ?? null,
1513
+ degreDansSigne: Number(p.degree?.toFixed(4)),
1514
+ retrograde: p.retrograde ?? false,
1515
+ ...p.house != null ? { maison: p.house } : {}
1516
+ }));
1517
+ }
1518
+ function jsonResult(value) {
1519
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
1520
+ }
1521
+ var server = new McpServer({
1522
+ name: "llmastro-ephemeris",
1523
+ version: "0.0.1"
1524
+ });
1525
+ server.tool(
1526
+ "get_planet_positions",
1527
+ "Positions \xE9cliptiques g\xE9ocentriques de tous les corps (Soleil\u2192Pluton, n\u0153uds, Lilith) \xE0 un instant donn\xE9. Calcul\xE9 c\xF4t\xE9 serveur par le moteur d'\xE9ph\xE9m\xE9ride \u2014 jamais estim\xE9 par le mod\xE8le.",
1528
+ {
1529
+ datetime: z.string().describe('Instant en ISO 8601 UTC, ex. "2026-07-18T14:00:00Z". Sans fuseau, interpr\xE9t\xE9 en UTC.')
1530
+ },
1531
+ async ({ datetime }) => {
1532
+ const jd2 = jdFromDate(parseUtc(datetime));
1533
+ const positions = allPositions2(jd2);
1534
+ return jsonResult({
1535
+ datetimeUTC: parseUtc(datetime).toISOString(),
1536
+ jd: jd2,
1537
+ moteur: getActiveEngine(),
1538
+ corps: formatPlanets(positions)
1539
+ });
1540
+ }
1541
+ );
1542
+ server.tool(
1543
+ "get_moon_phase",
1544
+ "Phase de la Lune (nom, illumination %, description) \xE0 un instant donn\xE9, d\xE9riv\xE9e de la position r\xE9elle Soleil/Lune.",
1545
+ {
1546
+ datetime: z.string().describe(`Instant en ISO 8601 UTC, ex. "2026-07-18T14:00:00Z". D\xE9faut d'interpr\xE9tation : UTC.`)
1547
+ },
1548
+ async ({ datetime }) => {
1549
+ const jd2 = jdFromDate(parseUtc(datetime));
1550
+ const mp = moonPhase2(jd2);
1551
+ return jsonResult({
1552
+ datetimeUTC: parseUtc(datetime).toISOString(),
1553
+ jd: jd2,
1554
+ moteur: getActiveEngine(),
1555
+ phase: mp.phase,
1556
+ emoji: mp.emoji,
1557
+ illumination: Number((mp.illumination * 100).toFixed(1)) + " %",
1558
+ description: mp.description
1559
+ });
1560
+ }
1561
+ );
1562
+ server.tool(
1563
+ "get_natal_chart",
1564
+ "Th\xE8me natal complet : positions plan\xE9taires en maisons, aspects, ascendant/MC, phase lunaire. Prend une date/heure LOCALE + fuseau IANA + coordonn\xE9es. Conversion UTC et calcul faits c\xF4t\xE9 serveur.",
1565
+ {
1566
+ date: z.string().describe('Date de naissance locale, format YYYY-MM-DD, ex. "1990-05-21".'),
1567
+ time: z.string().describe('Heure de naissance locale, format HH:mm (24 h), ex. "14:30".'),
1568
+ timezone: z.string().describe('Fuseau IANA du lieu de naissance, ex. "Europe/Paris", "America/New_York".'),
1569
+ latitude: z.number().min(-90).max(90).describe("Latitude en degr\xE9s d\xE9cimaux (positif = Nord)."),
1570
+ longitude: z.number().min(-180).max(180).describe("Longitude en degr\xE9s d\xE9cimaux (positif = Est).")
1571
+ },
1572
+ async ({ date, time, timezone, latitude, longitude }) => {
1573
+ const { jdUT, utcISO, offsetMinutes } = localToUTC(date, time, timezone);
1574
+ const chart = computeChartFromJD2(jdUT, latitude, longitude);
1575
+ return jsonResult({
1576
+ naissance: { date, time, timezone, latitude, longitude },
1577
+ utcISO,
1578
+ offsetMinutes,
1579
+ jd: jdUT,
1580
+ moteur: getActiveEngine(),
1581
+ ascendant: {
1582
+ longitude: Number(chart.houses.asc?.toFixed(4)),
1583
+ signe: SIGNS_FR[Math.floor((chart.houses.asc ?? 0) / 30) % 12] ?? null
1584
+ },
1585
+ milieuDuCiel: {
1586
+ longitude: Number(chart.houses.mc?.toFixed(4)),
1587
+ signe: SIGNS_FR[Math.floor((chart.houses.mc ?? 0) / 30) % 12] ?? null
1588
+ },
1589
+ systemeMaisons: chart.houseSystem,
1590
+ zodiaque: chart.zodiac,
1591
+ phaseLunaire: { phase: chart.moonPhase.phase, illumination: chart.moonPhase.illumination },
1592
+ retrogrades: chart.retrogrades,
1593
+ corps: formatPlanets(chart.planets),
1594
+ aspects: chart.aspects
1595
+ });
1596
+ }
1597
+ );
1598
+ server.tool(
1599
+ "get_current_sky",
1600
+ "Le ciel \xE0 l'instant pr\xE9sent (transits du moment) pour un lieu donn\xE9 : positions en maisons + aspects.",
1601
+ {
1602
+ latitude: z.number().min(-90).max(90).describe("Latitude en degr\xE9s d\xE9cimaux (positif = Nord)."),
1603
+ longitude: z.number().min(-180).max(180).describe("Longitude en degr\xE9s d\xE9cimaux (positif = Est).")
1604
+ },
1605
+ async ({ latitude, longitude }) => {
1606
+ const chart = computeCurrentSky2(latitude, longitude);
1607
+ return jsonResult({
1608
+ instant: (/* @__PURE__ */ new Date()).toISOString(),
1609
+ jd: chart.JD,
1610
+ moteur: getActiveEngine(),
1611
+ retrogrades: chart.retrogrades,
1612
+ phaseLunaire: { phase: chart.moonPhase.phase, illumination: chart.moonPhase.illumination },
1613
+ corps: formatPlanets(chart.planets),
1614
+ aspects: chart.aspects
1615
+ });
1616
+ }
1617
+ );
1618
+ server.tool(
1619
+ "get_astrocartography",
1620
+ "Astrocartographie \xE0 un instant donn\xE9 : pour chaque corps, les m\xE9ridiens MC/IC et les courbes lever/coucher, plus les parans (croisements de lignes). Utile pour \xAB o\xF9 telle plan\xE8te est-elle forte \xBB.",
1621
+ {
1622
+ datetime: z.string().describe('Instant en ISO 8601 UTC, ex. "2026-07-18T14:00:00Z". Sans fuseau, interpr\xE9t\xE9 en UTC.')
1623
+ },
1624
+ async ({ datetime }) => {
1625
+ const jd2 = jdFromDate(parseUtc(datetime));
1626
+ const acg = computeAstrocartography(jd2);
1627
+ return jsonResult({
1628
+ datetimeUTC: parseUtc(datetime).toISOString(),
1629
+ jd: jd2,
1630
+ moteur: getActiveEngine(),
1631
+ // Résumé lisible : on omet les tableaux de points (courbes échantillonnées),
1632
+ // on garde les méridiens et le nombre de points par courbe.
1633
+ lignes: acg.lines.map((l) => ({
1634
+ corps: l.key,
1635
+ meridienMC: Number(l.mcLng?.toFixed(4)),
1636
+ meridienIC: Number(l.icLng?.toFixed(4)),
1637
+ pointsLever: l.asc.length,
1638
+ pointsCoucher: l.dsc.length
1639
+ })),
1640
+ parans: acg.parans.map((p) => ({
1641
+ corpsA: p.aKey,
1642
+ angleA: p.aAngle,
1643
+ corpsB: p.bKey,
1644
+ angleB: p.bAngle,
1645
+ latitude: Number(p.lat.toFixed(3)),
1646
+ longitude: Number(p.lng.toFixed(3))
1647
+ }))
1648
+ });
1649
+ }
1650
+ );
1651
+ var NOISE_KEYS = /* @__PURE__ */ new Set(["fortune", "southNode", "ceres", "pallas", "juno", "vesta", "lilithTrue"]);
1652
+ function crossAspects(transit, natal) {
1653
+ const out = [];
1654
+ for (const [tk, tp] of Object.entries(transit)) {
1655
+ if (NOISE_KEYS.has(tk)) continue;
1656
+ for (const [nk, np] of Object.entries(natal)) {
1657
+ if (NOISE_KEYS.has(nk)) continue;
1658
+ let d = Math.abs(tp.longitude - np.longitude);
1659
+ if (d > 180) d = 360 - d;
1660
+ for (const a of ASPECT_TYPES) {
1661
+ const isLum = tk === "sun" || tk === "moon" || nk === "sun" || nk === "moon";
1662
+ const maxOrb = a.orb + (isLum ? 2 : 0);
1663
+ const o = Math.abs(d - a.angle);
1664
+ if (o <= maxOrb) {
1665
+ out.push({
1666
+ planeteTransit: tk,
1667
+ aspect: a.nameFr,
1668
+ symbole: a.symbol,
1669
+ planeteNatale: nk,
1670
+ orbe: Number(o.toFixed(2)),
1671
+ exact: o < 1
1672
+ });
1673
+ }
1674
+ }
1675
+ }
1676
+ }
1677
+ return out.sort((x, y) => x.orbe - y.orbe);
1678
+ }
1679
+ server.tool(
1680
+ "get_transits",
1681
+ "Transits du moment crois\xE9s au th\xE8me natal : aspects entre les plan\xE8tes qui transitent aujourd'hui (ou \xE0 une date donn\xE9e) et les plan\xE8tes de naissance. Le c\u0153ur des \xAB pr\xE9visions \xBB.",
1682
+ {
1683
+ date: z.string().describe("Date de naissance locale, YYYY-MM-DD."),
1684
+ time: z.string().describe("Heure de naissance locale, HH:mm."),
1685
+ timezone: z.string().describe('Fuseau IANA du lieu de naissance, ex. "Europe/Paris".'),
1686
+ latitude: z.number().min(-90).max(90).describe("Latitude de naissance (deg d\xE9cimaux)."),
1687
+ longitude: z.number().min(-180).max(180).describe("Longitude de naissance (deg d\xE9cimaux)."),
1688
+ datetime: z.string().optional().describe('Instant du transit en ISO UTC (d\xE9faut : maintenant), ex. "2026-07-19T09:00:00Z".')
1689
+ },
1690
+ async ({ date, time, timezone, latitude, longitude, datetime }) => {
1691
+ const { jdUT: natalJd } = localToUTC(date, time, timezone);
1692
+ const natal = computeChartFromJD2(natalJd, latitude, longitude).planets;
1693
+ const transitDate = datetime ? parseUtc(datetime) : /* @__PURE__ */ new Date();
1694
+ const transit = allPositions2(jdFromDate(transitDate));
1695
+ const aspects = crossAspects(transit, natal);
1696
+ return jsonResult({
1697
+ naissance: { date, time, timezone },
1698
+ instantTransit: transitDate.toISOString(),
1699
+ moteur: getActiveEngine(),
1700
+ nbAspects: aspects.length,
1701
+ aspects
1702
+ });
1703
+ }
1704
+ );
1705
+ var HORIZONS_API = "https://ssd.jpl.nasa.gov/api/horizons.api";
1706
+ var HORIZONS_CMD = {
1707
+ sun: "10",
1708
+ moon: "301",
1709
+ mercury: "199",
1710
+ venus: "299",
1711
+ mars: "4",
1712
+ jupiter: "5",
1713
+ saturn: "6",
1714
+ uranus: "7",
1715
+ neptune: "8",
1716
+ pluto: "9"
1717
+ };
1718
+ async function horizonsLongitude(command, jd2) {
1719
+ const params = new URLSearchParams({
1720
+ format: "text",
1721
+ COMMAND: `'${command}'`,
1722
+ OBJ_DATA: "'NO'",
1723
+ MAKE_EPHEM: "'YES'",
1724
+ EPHEM_TYPE: "'OBSERVER'",
1725
+ CENTER: "'500@399'",
1726
+ TLIST: `'${jd2}'`,
1727
+ TLIST_TYPE: "'JD'",
1728
+ QUANTITIES: "'31'",
1729
+ CSV_FORMAT: "'YES'",
1730
+ ANG_FORMAT: "'DEG'"
1731
+ });
1732
+ let lastErr;
1733
+ for (let attempt = 0; attempt < 3; attempt++) {
1734
+ if (attempt > 0) await new Promise((r) => setTimeout(r, 400 * attempt));
1735
+ const res = await fetch(`${HORIZONS_API}?${params}`);
1736
+ if (res.status === 503 || res.status === 429) {
1737
+ lastErr = new Error(`HTTP ${res.status}`);
1738
+ continue;
1739
+ }
1740
+ if (!res.ok) throw new Error(`Horizons HTTP ${res.status} (COMMAND=${command})`);
1741
+ const text = await res.text();
1742
+ const block = text.split("$$SOE")[1]?.split("$$EOE")[0];
1743
+ if (!block) throw new Error(`Bloc SOE/EOE absent (COMMAND=${command})`);
1744
+ const cols = block.trim().split("\n")[0].split(",").map((s) => s.trim());
1745
+ const lon = Number(cols[3]);
1746
+ if (!Number.isFinite(lon)) throw new Error(`Longitude illisible (COMMAND=${command})`);
1747
+ return lon;
1748
+ }
1749
+ throw new Error(`Horizons indisponible apr\xE8s 3 tentatives (COMMAND=${command}) : ${String(lastErr)}`);
1750
+ }
1751
+ function arcsecDelta(a, b) {
1752
+ let d = a - b;
1753
+ d = ((d + 180) % 360 + 360) % 360 - 180;
1754
+ return d * 3600;
1755
+ }
1756
+ server.tool(
1757
+ "validate_against_horizons",
1758
+ "Preuve d'exactitude : compare nos positions plan\xE9taires \xE0 la r\xE9f\xE9rence mondiale JPL Horizons (NASA) au m\xEAme instant, et donne l'\xE9cart en secondes d'arc. N\xE9cessite un acc\xE8s r\xE9seau.",
1759
+ {
1760
+ datetime: z.string().describe('Instant en ISO 8601 UTC, ex. "2026-07-19T00:00:00Z".'),
1761
+ bodies: z.array(z.enum(["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"])).optional().describe("Sous-ensemble de corps \xE0 valider (d\xE9faut : les 10 classiques).")
1762
+ },
1763
+ async ({ datetime, bodies }) => {
1764
+ const jd2 = jdFromDate(parseUtc(datetime));
1765
+ const ours = allPositions2(jd2);
1766
+ const keys = bodies ?? Object.keys(HORIZONS_CMD);
1767
+ const rows = await Promise.all(
1768
+ keys.map(async (k) => {
1769
+ try {
1770
+ const ref = await horizonsLongitude(HORIZONS_CMD[k], jd2);
1771
+ const mine = ours[k]?.longitude;
1772
+ if (mine == null) return { corps: k, erreur: "absent de notre moteur" };
1773
+ const ecart = arcsecDelta(mine, ref);
1774
+ return {
1775
+ corps: k,
1776
+ notreLongitude: Number(mine.toFixed(5)),
1777
+ horizonsLongitude: Number(ref.toFixed(5)),
1778
+ ecartArcsec: Number(ecart.toFixed(1))
1779
+ };
1780
+ } catch (e) {
1781
+ return { corps: k, erreur: String(e?.message ?? e) };
1782
+ }
1783
+ })
1784
+ );
1785
+ const ecarts = rows.map((r) => Math.abs(r.ecartArcsec)).filter((n) => Number.isFinite(n));
1786
+ return jsonResult({
1787
+ datetimeUTC: parseUtc(datetime).toISOString(),
1788
+ jd: jd2,
1789
+ moteur: getActiveEngine(),
1790
+ source: "JPL Horizons (ssd.jpl.nasa.gov), quantit\xE9 31 \u2014 ObsEcLon g\xE9ocentrique apparente",
1791
+ resume: ecarts.length ? {
1792
+ ecartMaxArcsec: Number(Math.max(...ecarts).toFixed(1)),
1793
+ ecartMoyenArcsec: Number((ecarts.reduce((a, b) => a + b, 0) / ecarts.length).toFixed(1))
1794
+ } : null,
1795
+ note: getActiveEngine() === "astracore" ? "Mode AstraCore : petits \xE9carts syst\xE9matiques attendus (\u0394T non appliqu\xE9, surtout sur la Lune). Passe en ASTRO_ENGINE=swisseph pour un match sub-arcseconde." : "Mode swisseph : match attendu au niveau de la seconde d'arc.",
1796
+ corps: rows
1797
+ });
1798
+ }
1799
+ );
1800
+ async function main() {
1801
+ const transport = new StdioServerTransport();
1802
+ await server.connect(transport);
1803
+ console.error(`[mcp-ephemeris] pr\xEAt \xB7 moteur actif = ${getActiveEngine()}`);
1804
+ }
1805
+ main().catch((err) => {
1806
+ console.error("[mcp-ephemeris] \xE9chec au d\xE9marrage :", err);
1807
+ process.exit(1);
1808
+ });