@cosmosos/mcp-ephemeris 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +32 -3
  2. package/dist/index.js +1262 -115
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -242,8 +242,8 @@ function helioToGeoLonLat(p, e) {
242
242
  lat: Math.atan2(zg, Math.hypot(xg, yg)) * D
243
243
  };
244
244
  }
245
- function isRetrograde(key, JD) {
246
- if (!(key in PE)) return false;
245
+ function angularSpeed(key, JD) {
246
+ if (!(key in PE)) return void 0;
247
247
  const T1 = (JD - 0.5 - 2451545) / 36525;
248
248
  const T2 = (JD + 0.5 - 2451545) / 36525;
249
249
  const e1 = helioPos3D("earth", T1), e2 = helioPos3D("earth", T2);
@@ -253,7 +253,11 @@ function isRetrograde(key, JD) {
253
253
  let diff = g2 - g1;
254
254
  if (diff > 180) diff -= 360;
255
255
  if (diff < -180) diff += 360;
256
- return diff < 0;
256
+ return diff;
257
+ }
258
+ function isRetrograde(key, JD) {
259
+ const v = angularSpeed(key, JD);
260
+ return v !== void 0 && v < 0;
257
261
  }
258
262
 
259
263
  // ../ephemeris/src/solar-lunar.ts
@@ -406,8 +410,18 @@ function placidusHouses(RAMC, lat, ob) {
406
410
  c[5] = n360(c[11] + 180);
407
411
  c[7] = n360(c[1] + 180);
408
412
  c[8] = n360(c[2] + 180);
413
+ if (!cuspsInZodiacalOrder(c)) return equalHouses(asc);
409
414
  return c;
410
415
  }
416
+ function cuspsInZodiacalOrder(c) {
417
+ let sum = 0;
418
+ for (let i = 0; i < 12; i++) {
419
+ const arc = n360(c[(i + 1) % 12] - c[i]);
420
+ if (arc <= 0 || arc >= 180) return false;
421
+ sum += arc;
422
+ }
423
+ return Math.abs(sum - 360) < 1e-6;
424
+ }
411
425
  function equalHouses(asc) {
412
426
  return Array.from({ length: 12 }, (_, i) => n360(asc + i * 30));
413
427
  }
@@ -415,8 +429,32 @@ function wholeSignHouses(asc) {
415
429
  const base = Math.floor(asc / 30) * 30;
416
430
  return Array.from({ length: 12 }, (_, i) => n360(base + i * 30));
417
431
  }
418
- function kochHouses(RAMC, lat, ob) {
419
- return placidusHouses(RAMC, lat, ob);
432
+ var _houseSubstitutionWarned = /* @__PURE__ */ new Set();
433
+ function warnHouseSubstitutionOnce(system) {
434
+ if (_houseSubstitutionWarned.has(system)) return;
435
+ _houseSubstitutionWarned.add(system);
436
+ console.warn(
437
+ `[ephemeris/houses] Syst\xE8me "${system}" non disponible sur le moteur de secours AstraCore \u2014 repli honn\xEAte sur Placidus (system renvoy\xE9 = "placidus"). Le vrai calcul n\xE9cessite Swiss Ephemeris.`
438
+ );
439
+ }
440
+ function porphyryHouses(asc, mc) {
441
+ const ic = n360(mc + 180);
442
+ const dsc = n360(asc + 180);
443
+ const c = new Array(12);
444
+ c[0] = asc;
445
+ c[3] = ic;
446
+ c[6] = dsc;
447
+ c[9] = mc;
448
+ const tris = (from, to, i1, i2) => {
449
+ const arc = n360(to - from);
450
+ c[i1] = n360(from + arc / 3);
451
+ c[i2] = n360(from + 2 * arc / 3);
452
+ };
453
+ tris(asc, ic, 1, 2);
454
+ tris(ic, dsc, 4, 5);
455
+ tris(dsc, mc, 7, 8);
456
+ tris(mc, asc, 10, 11);
457
+ return c;
420
458
  }
421
459
  function calculateHousesByCoords(system, JD, lat, lng) {
422
460
  const T = (JD - 2451545) / 36525;
@@ -425,6 +463,7 @@ function calculateHousesByCoords(system, JD, lat, lng) {
425
463
  const asc = calcASC(RA, lat, ob);
426
464
  const mc = calcMC(RA, ob);
427
465
  let cusps;
466
+ let effectiveSystem = system;
428
467
  switch (system) {
429
468
  case "equal":
430
469
  cusps = equalHouses(asc);
@@ -432,14 +471,24 @@ function calculateHousesByCoords(system, JD, lat, lng) {
432
471
  case "whole_sign":
433
472
  cusps = wholeSignHouses(asc);
434
473
  break;
474
+ // HOUSE-SYSTEMS-EXTEND-V1 : Porphyry = trisection pure des quadrants
475
+ // ASC→IC→DSC→MC. Exact sans trigonométrie sphérique, donc calculable
476
+ // aussi en AstraCore (pas de substitution).
477
+ case "porphyry":
478
+ cusps = porphyryHouses(asc, mc);
479
+ break;
435
480
  case "koch":
436
- cusps = kochHouses(RA, lat, ob);
481
+ case "regiomontanus":
482
+ case "campanus":
483
+ warnHouseSubstitutionOnce(system);
484
+ cusps = placidusHouses(RA, lat, ob);
485
+ effectiveSystem = "placidus";
437
486
  break;
438
487
  case "placidus":
439
488
  default:
440
489
  cusps = placidusHouses(RA, lat, ob);
441
490
  }
442
- return { cusps, asc, mc, vertex: null, system };
491
+ return { cusps, asc, mc, vertex: null, system: effectiveSystem };
443
492
  }
444
493
  function houseOfLongitude(lon, cusps) {
445
494
  for (let i = 0; i < 12; i++) {
@@ -531,41 +580,112 @@ function computeHermeticLots(asc, planets, isNight, fortune) {
531
580
  }
532
581
 
533
582
  // ../ephemeris/src/astro-engine.ts
583
+ function deltaTSeconds(JD) {
584
+ const year = 2e3 + (JD - 2451545) / 365.25;
585
+ const longTerm = (y) => {
586
+ const u = (y - 1820) / 100;
587
+ return -20 + 32 * u * u;
588
+ };
589
+ if (year < 1800 || year >= 2150) return longTerm(year);
590
+ let t;
591
+ if (year < 1860) {
592
+ t = year - 1800;
593
+ return 13.72 - 0.332447 * t + 68612e-7 * t ** 2 + 41116e-7 * t ** 3 - 37436e-8 * t ** 4 + 121272e-10 * t ** 5 - 1699e-10 * t ** 6 + 875e-12 * t ** 7;
594
+ }
595
+ if (year < 1900) {
596
+ t = year - 1860;
597
+ return 7.62 + 0.5737 * t - 0.251754 * t ** 2 + 0.01680668 * t ** 3 - 4473624e-10 * t ** 4 + t ** 5 / 233174;
598
+ }
599
+ if (year < 1920) {
600
+ t = year - 1900;
601
+ return -2.79 + 1.494119 * t - 0.0598939 * t ** 2 + 61966e-7 * t ** 3 - 197e-6 * t ** 4;
602
+ }
603
+ if (year < 1941) {
604
+ t = year - 1920;
605
+ return 21.2 + 0.84493 * t - 0.0761 * t ** 2 + 20936e-7 * t ** 3;
606
+ }
607
+ if (year < 1961) {
608
+ t = year - 1950;
609
+ return 29.07 + 0.407 * t - t ** 2 / 233 + t ** 3 / 2547;
610
+ }
611
+ if (year < 1986) {
612
+ t = year - 1975;
613
+ return 45.45 + 1.067 * t - t ** 2 / 260 - t ** 3 / 718;
614
+ }
615
+ if (year < 2005) {
616
+ t = year - 2e3;
617
+ return 63.86 + 0.3345 * t - 0.060374 * t ** 2 + 17275e-7 * t ** 3 + 651814e-9 * t ** 4 + 2373599e-11 * t ** 5;
618
+ }
619
+ if (year < 2050) {
620
+ t = year - 2e3;
621
+ return 62.92 + 0.32217 * t + 5589e-6 * t ** 2;
622
+ }
623
+ return -20 + 32 * ((year - 1820) / 100) ** 2 - 0.5628 * (2150 - year);
624
+ }
625
+ function ttCenturiesFromUT(jdUT) {
626
+ return (jdUT + deltaTSeconds(jdUT) / 86400 - 2451545) / 36525;
627
+ }
534
628
  function allPositions(JD) {
535
- const T = (JD - 2451545) / 36525;
629
+ const T = ttCenturiesFromUT(JD);
536
630
  const pos = {};
631
+ const HALF_DAY_T = 0.5 / 36525;
632
+ const speedOf = (lonAt) => {
633
+ let diff = lonAt(T + HALF_DAY_T) - lonAt(T - HALF_DAY_T);
634
+ if (diff > 180) diff -= 360;
635
+ if (diff < -180) diff += 360;
636
+ return diff;
637
+ };
537
638
  const sun = sunGeo(T);
639
+ const sunSpeed = speedOf((t) => sunGeo(t).lon);
538
640
  pos["sun"] = {
539
641
  key: "sun",
540
642
  longitude: sun.lon,
541
643
  signIdx: Math.floor(sun.lon / 30),
542
- degree: sun.lon % 30
644
+ degree: sun.lon % 30,
645
+ speed: sunSpeed,
646
+ retrograde: false
647
+ // le Soleil apparent ne rétrograde jamais
543
648
  };
544
649
  const ml = moonGeo(T);
650
+ const moonSpeed = speedOf((t) => moonGeo(t));
545
651
  pos["moon"] = {
546
652
  key: "moon",
547
653
  longitude: ml,
548
654
  signIdx: Math.floor(ml / 30),
549
- degree: ml % 30
655
+ degree: ml % 30,
656
+ speed: moonSpeed,
657
+ retrograde: false
658
+ // la Lune non plus
550
659
  };
551
660
  const earthH = helioPos3D("earth", T);
661
+ const earthH1 = helioPos3D("earth", T - HALF_DAY_T);
662
+ const earthH2 = helioPos3D("earth", T + HALF_DAY_T);
552
663
  OUTER_PLANETS.forEach((k) => {
553
664
  const pH = helioPos3D(k, T);
554
665
  const lon = helioToGeoLon(pH, earthH);
666
+ const g1 = helioToGeoLon(helioPos3D(k, T - HALF_DAY_T), earthH1);
667
+ const g2 = helioToGeoLon(helioPos3D(k, T + HALF_DAY_T), earthH2);
668
+ let speed = g2 - g1;
669
+ if (speed > 180) speed -= 360;
670
+ if (speed < -180) speed += 360;
555
671
  pos[k] = {
556
672
  key: k,
557
673
  longitude: lon,
558
674
  signIdx: Math.floor(lon / 30),
559
- degree: lon % 30
675
+ degree: lon % 30,
676
+ speed,
677
+ retrograde: speed < 0
560
678
  };
561
679
  });
562
680
  const nn = lunarNode(T);
681
+ const nodeSpeed = speedOf((t) => lunarNode(t));
563
682
  pos["northNode"] = {
564
683
  key: "northNode",
565
684
  longitude: nn,
566
685
  signIdx: Math.floor(nn / 30),
567
686
  degree: nn % 30,
568
- retrograde: true
687
+ retrograde: true,
688
+ speed: nodeSpeed
569
689
  };
570
690
  const sn = n360(nn + 180);
571
691
  pos["southNode"] = {
@@ -573,7 +693,8 @@ function allPositions(JD) {
573
693
  longitude: sn,
574
694
  signIdx: Math.floor(sn / 30),
575
695
  degree: sn % 30,
576
- retrograde: true
696
+ retrograde: true,
697
+ speed: nodeSpeed
577
698
  };
578
699
  return pos;
579
700
  }
@@ -590,7 +711,7 @@ var ACG_BODY_KEYS = [
590
711
  "pluto"
591
712
  ];
592
713
  function equatorialPositions(JD) {
593
- const T = (JD - 2451545) / 36525;
714
+ const T = ttCenturiesFromUT(JD);
594
715
  const eps = obl(T);
595
716
  const out = {};
596
717
  out["sun"] = eclipticToEquatorial(sunGeo(T).lon, 0, eps);
@@ -640,8 +761,12 @@ function computeChartFromJD(JD, latitude, longitude, opts = {}) {
640
761
  }
641
762
  const retrogrades = [];
642
763
  ["mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"].forEach((k) => {
643
- const r = isRetrograde(k, JD);
644
- if (planets[k]) planets[k].retrograde = r;
764
+ const v = angularSpeed(k, JD);
765
+ const r = v !== void 0 && v < 0;
766
+ if (planets[k]) {
767
+ planets[k].retrograde = r;
768
+ if (v !== void 0) planets[k].speed = v;
769
+ }
645
770
  if (r) retrogrades.push(k);
646
771
  });
647
772
  const aspects = calculateAspects(planets);
@@ -704,6 +829,9 @@ function loadSwisseph() {
704
829
  return false;
705
830
  }
706
831
  }
832
+ function isSwissephLoaded() {
833
+ return _swe !== null && _swe !== false;
834
+ }
707
835
  function getSwissephLoadError() {
708
836
  return _swissephLoadError;
709
837
  }
@@ -783,7 +911,11 @@ var HOUSE_SYSTEM_CHAR = {
783
911
  placidus: "P",
784
912
  koch: "K",
785
913
  equal: "E",
786
- whole_sign: "W"
914
+ whole_sign: "W",
915
+ // HOUSE-SYSTEMS-EXTEND-V1 : codes swisseph natifs.
916
+ porphyry: "O",
917
+ regiomontanus: "R",
918
+ campanus: "C"
787
919
  };
788
920
  function allPositionsSwiss(JD) {
789
921
  if (!loadSwisseph()) throw new Error("swisseph not loaded");
@@ -806,12 +938,14 @@ function allPositionsSwiss(JD) {
806
938
  }
807
939
  throw new Error(`swe_calc_ut failed for ${key} (ipl=${code}): ${errored ? r.error : "longitude non finie"}`);
808
940
  }
941
+ const speed = typeof r.longitudeSpeed === "number" && Number.isFinite(r.longitudeSpeed) ? r.longitudeSpeed : void 0;
809
942
  out[key] = {
810
943
  key,
811
944
  longitude: lon,
812
945
  signIdx: Math.floor(lon / 30),
813
946
  degree: lon % 30,
814
- retrograde: typeof r.longitudeSpeed === "number" ? r.longitudeSpeed < 0 : false
947
+ retrograde: speed !== void 0 ? speed < 0 : false,
948
+ ...speed !== void 0 ? { speed } : {}
815
949
  };
816
950
  }
817
951
  if (out["northNode"]) {
@@ -824,7 +958,9 @@ function allPositionsSwiss(JD) {
824
958
  // SOUTH-NODE-RETRO-FIX : le Nœud Sud partage l'axe nodal — il a
825
959
  // donc le même mouvement que le Nœud Nord. Sans cette ligne il
826
960
  // s'affichait « Direct » alors que le Nœud Nord est rétrograde.
827
- retrograde: out["northNode"].retrograde ?? false
961
+ retrograde: out["northNode"].retrograde ?? false,
962
+ // TRANSIT-SPEED-V1 : longitude = nord + 180 ⇒ dérivée identique.
963
+ ...out["northNode"].speed !== void 0 ? { speed: out["northNode"].speed } : {}
828
964
  };
829
965
  }
830
966
  return out;
@@ -991,6 +1127,23 @@ function computeCurrentSkySwiss(lat, lng, opts = {}) {
991
1127
  const JD = Date.now() / 864e5 + 24405875e-1;
992
1128
  return computeChartFromJDSwiss(JD, lat, lng, opts);
993
1129
  }
1130
+ function isRetrogradeSwiss(key, JD) {
1131
+ if (!loadSwisseph()) throw new Error("swisseph not loaded");
1132
+ const code = getPlanetIpl()[key];
1133
+ if (typeof code !== "number") return false;
1134
+ const flag = (FILE_BASED_BODIES.has(key) ? _swe.SEFLG_SWIEPH : _swe.SEFLG_MOSEPH) | _swe.SEFLG_SPEED;
1135
+ let r;
1136
+ try {
1137
+ r = _swe.swe_calc_ut(JD, code, flag);
1138
+ } catch (err) {
1139
+ r = { error: err instanceof Error ? err.message : String(err) };
1140
+ }
1141
+ if (r && typeof r === "object" && "error" in r && r.error) {
1142
+ if (OPTIONAL_BODIES.has(key)) return false;
1143
+ throw new Error(`swe_calc_ut failed for ${key}: ${r.error}`);
1144
+ }
1145
+ return typeof r.longitudeSpeed === "number" ? r.longitudeSpeed < 0 : false;
1146
+ }
994
1147
  function moonPhaseSwiss(JD) {
995
1148
  if (!loadSwisseph()) throw new Error("swisseph not loaded");
996
1149
  const ipl = getPlanetIpl();
@@ -998,6 +1151,83 @@ function moonPhaseSwiss(JD) {
998
1151
  const moon = _swe.swe_calc_ut(JD, ipl.moon, _swe.SEFLG_MOSEPH);
999
1152
  return moonPhaseFromLongitudes(n360(sun.longitude), n360(moon.longitude));
1000
1153
  }
1154
+ function decodeSolarRflag(rflag, swe) {
1155
+ if (rflag & (swe.SE_ECL_ANNULAR_TOTAL ?? 8)) return "hybrid";
1156
+ if (rflag & (swe.SE_ECL_TOTAL ?? 1)) return "total";
1157
+ if (rflag & (swe.SE_ECL_ANNULAR ?? 2)) return "annular";
1158
+ return "partial";
1159
+ }
1160
+ function decodeLunarRflag(rflag, swe) {
1161
+ if (rflag & (swe.SE_ECL_TOTAL ?? 1)) return "total";
1162
+ if (rflag & (swe.SE_ECL_PARTIAL ?? 4)) return "partial";
1163
+ if (rflag & (swe.SE_ECL_PENUMBRAL ?? 16)) return "penumbral";
1164
+ return "penumbral";
1165
+ }
1166
+ function computeSolarEclipseDetailsSwiss(JD) {
1167
+ if (!loadSwisseph()) return null;
1168
+ try {
1169
+ const r = _swe.swe_sol_eclipse_where(JD, _swe.SEFLG_MOSEPH);
1170
+ if (!r || "error" in r) return null;
1171
+ return {
1172
+ magnitude: r.eclipseMagnitude,
1173
+ obscuration: r.solarDiscFraction,
1174
+ kind: decodeSolarRflag(r.rflag, _swe),
1175
+ saros: r.sarosNumber,
1176
+ sarosMember: r.sarosMember
1177
+ };
1178
+ } catch {
1179
+ return null;
1180
+ }
1181
+ }
1182
+ function computeLunarEclipseDetailsSwiss(JD) {
1183
+ if (!loadSwisseph()) return null;
1184
+ try {
1185
+ const r = _swe.swe_lun_eclipse_how(JD, _swe.SEFLG_MOSEPH, 0, 0, 0);
1186
+ if (!r || "error" in r) return null;
1187
+ return {
1188
+ magnitude: r.umbralMagnitude,
1189
+ penumbralMagnitude: r.penumbralMagnitude,
1190
+ kind: decodeLunarRflag(r.rflag, _swe),
1191
+ saros: r.sarosNumber,
1192
+ sarosMember: r.sarosMember
1193
+ };
1194
+ } catch {
1195
+ return null;
1196
+ }
1197
+ }
1198
+ var numOrNull = (v) => typeof v === "number" ? v : null;
1199
+ function findNextSolarEclipseSwiss(jdStart, backward = false) {
1200
+ if (!loadSwisseph()) return null;
1201
+ try {
1202
+ const r = _swe.swe_sol_eclipse_when_glob(jdStart, _swe.SEFLG_MOSEPH, 0, backward ? 1 : 0);
1203
+ if (!r || "error" in r || typeof r.maximum !== "number") return null;
1204
+ return {
1205
+ jdMaximum: r.maximum,
1206
+ kind: decodeSolarRflag(r.rflag, _swe),
1207
+ begin: numOrNull(r.begin),
1208
+ end: numOrNull(r.end)
1209
+ };
1210
+ } catch {
1211
+ return null;
1212
+ }
1213
+ }
1214
+ function findNextLunarEclipseSwiss(jdStart, backward = false) {
1215
+ if (!loadSwisseph()) return null;
1216
+ try {
1217
+ const r = _swe.swe_lun_eclipse_when(jdStart, _swe.SEFLG_MOSEPH, 0, backward ? 1 : 0);
1218
+ if (!r || "error" in r || typeof r.maximum !== "number") return null;
1219
+ return {
1220
+ jdMaximum: r.maximum,
1221
+ kind: decodeLunarRflag(r.rflag, _swe),
1222
+ partialBegin: numOrNull(r.partialBegin),
1223
+ partialEnd: numOrNull(r.partialEnd),
1224
+ penumbralBegin: numOrNull(r.penumbralBegin),
1225
+ penumbralEnd: numOrNull(r.penumbralEnd)
1226
+ };
1227
+ } catch {
1228
+ return null;
1229
+ }
1230
+ }
1001
1231
 
1002
1232
  // ../ephemeris/src/engine-router.ts
1003
1233
  var _activeEngine = null;
@@ -1031,6 +1261,16 @@ function resolveEngine() {
1031
1261
  function getActiveEngine() {
1032
1262
  return resolveEngine();
1033
1263
  }
1264
+ function getEngineDiagnostic() {
1265
+ const active = resolveEngine();
1266
+ return {
1267
+ active,
1268
+ requested: (process.env["ASTRO_ENGINE"] ?? "swisseph").toLowerCase().trim(),
1269
+ swissephLoaded: isSwissephLoaded(),
1270
+ swissephError: getSwissephLoadError(),
1271
+ resolutionLog: _resolutionLog ?? "(non r\xE9solu)"
1272
+ };
1273
+ }
1034
1274
  function computeChartFromJD2(JD, latitude, longitude, opts = {}) {
1035
1275
  if (resolveEngine() === "swisseph") {
1036
1276
  return computeChartFromJDSwiss(JD, latitude, longitude, opts);
@@ -1046,6 +1286,9 @@ function computeCurrentSky2(lat, lng, opts = {}) {
1046
1286
  function allPositions2(JD) {
1047
1287
  return resolveEngine() === "swisseph" ? allPositionsSwiss(JD) : allPositions(JD);
1048
1288
  }
1289
+ function isRetrograde3(key, JD) {
1290
+ return resolveEngine() === "swisseph" ? isRetrogradeSwiss(key, JD) : isRetrograde(key, JD);
1291
+ }
1049
1292
  function moonPhase2(JD) {
1050
1293
  return resolveEngine() === "swisseph" ? moonPhaseSwiss(JD) : moonPhase(JD);
1051
1294
  }
@@ -1077,6 +1320,419 @@ var CityNotFoundError = class extends Error {
1077
1320
  suggestions;
1078
1321
  };
1079
1322
 
1323
+ // ../ephemeris/src/planting-calendar.ts
1324
+ function R2(a, b) {
1325
+ const out = [];
1326
+ let m = a;
1327
+ for (let i = 0; i < 12; i++) {
1328
+ out.push(m);
1329
+ if (m === b) break;
1330
+ m = m === 12 ? 1 : m + 1;
1331
+ }
1332
+ return out;
1333
+ }
1334
+ var VEGETABLES = [
1335
+ // Racines / bulbes
1336
+ { key: "carrot", fr: "carottes", en: "carrots", category: "root", sow: R2(3, 7), harvest: R2(6, 11) },
1337
+ { key: "radish", fr: "radis", en: "radishes", category: "root", sow: R2(3, 9), harvest: R2(4, 10) },
1338
+ { key: "beetroot", fr: "betteraves", en: "beetroot", category: "root", sow: R2(4, 6), harvest: R2(7, 11) },
1339
+ { key: "turnip", fr: "navets", en: "turnips", category: "root", sow: R2(3, 9), harvest: R2(5, 11) },
1340
+ { key: "parsnip", fr: "panais", en: "parsnips", category: "root", sow: R2(3, 5), harvest: R2(10, 2) },
1341
+ { key: "potato", fr: "pommes de terre", en: "potatoes", category: "root", plant: R2(3, 5), harvest: R2(6, 9) },
1342
+ { key: "onion", fr: "oignons", en: "onions", category: "root", sow: R2(2, 4), plant: R2(3, 4), harvest: R2(7, 9) },
1343
+ { key: "garlic", fr: "ail", en: "garlic", category: "root", plant: [10, 11, 12, 2, 3], harvest: R2(6, 7) },
1344
+ { key: "shallot", fr: "\xE9chalotes", en: "shallots", category: "root", plant: R2(10, 3), harvest: R2(6, 7) },
1345
+ { key: "celeriac", fr: "c\xE9leri-rave", en: "celeriac", category: "root", sowUnder: R2(3, 4), plant: R2(5, 6), harvest: R2(9, 11) },
1346
+ { key: "jerusalem_artichoke", fr: "topinambours", en: "Jerusalem artichokes", category: "root", plant: R2(2, 4), harvest: R2(10, 3) },
1347
+ { key: "salsify", fr: "salsifis", en: "salsify", category: "root", sow: R2(4, 5), harvest: R2(10, 2) },
1348
+ // Feuilles
1349
+ { key: "lettuce", fr: "salades", en: "lettuces", category: "leaf", sowUnder: R2(2, 3), sow: R2(3, 9), harvest: R2(4, 11) },
1350
+ { key: "spinach", fr: "\xE9pinards", en: "spinach", category: "leaf", sow: [3, 4, 8, 9], harvest: R2(10, 6) },
1351
+ { key: "leek", fr: "poireaux", en: "leeks", category: "leaf", sow: R2(2, 4), plant: R2(6, 8), harvest: R2(9, 3) },
1352
+ { key: "chard", fr: "blettes", en: "chard", category: "leaf", sow: R2(4, 6), harvest: R2(7, 11) },
1353
+ { key: "lambs_lettuce", fr: "m\xE2che", en: "lamb's lettuce", category: "leaf", sow: R2(8, 9), harvest: R2(10, 3) },
1354
+ { key: "cabbage", fr: "choux pomm\xE9s", en: "cabbages", category: "leaf", sowUnder: R2(3, 4), plant: R2(5, 7), harvest: R2(9, 2) },
1355
+ { key: "kale", fr: "chou fris\xE9", en: "kale", category: "leaf", sow: R2(4, 6), plant: R2(6, 7), harvest: R2(10, 2) },
1356
+ { key: "brussels", fr: "choux de Bruxelles", en: "Brussels sprouts", category: "leaf", sowUnder: R2(3, 4), plant: R2(5, 6), harvest: R2(10, 2) },
1357
+ { key: "rocket", fr: "roquette", en: "rocket", category: "leaf", sow: R2(3, 9), harvest: R2(4, 10) },
1358
+ { key: "parsley", fr: "persil", en: "parsley", category: "leaf", sow: R2(3, 7), harvest: R2(5, 11) },
1359
+ { key: "chicory", fr: "endives", en: "chicory", category: "leaf", sow: R2(5, 6), harvest: R2(10, 2) },
1360
+ { key: "fennel", fr: "fenouil", en: "fennel", category: "leaf", sow: R2(5, 7), harvest: R2(8, 11) },
1361
+ { key: "basil", fr: "basilic", en: "basil", category: "leaf", sowUnder: R2(3, 4), plant: [5], harvest: R2(6, 9) },
1362
+ { key: "chives", fr: "ciboulette", en: "chives", category: "leaf", plant: R2(3, 5), harvest: R2(4, 10) },
1363
+ { key: "sorrel", fr: "oseille", en: "sorrel", category: "leaf", sow: R2(3, 5), harvest: R2(5, 10) },
1364
+ // Fleurs (comestibles / inflorescences)
1365
+ { key: "cauliflower", fr: "choux-fleurs", en: "cauliflowers", category: "flower", sowUnder: R2(3, 4), plant: R2(5, 6), harvest: R2(9, 11) },
1366
+ { key: "broccoli", fr: "brocolis", en: "broccoli", category: "flower", sow: R2(3, 5), plant: R2(5, 6), harvest: R2(8, 11) },
1367
+ { key: "artichoke", fr: "artichauts", en: "artichokes", category: "flower", plant: R2(3, 4), harvest: R2(6, 9) },
1368
+ // Fruits (légumes-fruits)
1369
+ { key: "tomato", fr: "tomates", en: "tomatoes", category: "fruit", sowUnder: R2(2, 4), plant: [5], harvest: R2(7, 10) },
1370
+ { key: "courgette", fr: "courgettes", en: "courgettes", category: "fruit", sowUnder: R2(3, 4), sow: [5], plant: R2(5, 6), harvest: R2(6, 10) },
1371
+ { key: "cucumber", fr: "concombres", en: "cucumbers", category: "fruit", sowUnder: R2(3, 4), plant: R2(5, 6), harvest: R2(7, 9) },
1372
+ { key: "gherkin", fr: "cornichons", en: "gherkins", category: "fruit", sow: [5], harvest: R2(7, 9) },
1373
+ { key: "squash", fr: "courges", en: "squash", category: "fruit", sowUnder: [4], sow: [5], harvest: R2(9, 11) },
1374
+ { key: "green_bean", fr: "haricots verts", en: "green beans", category: "fruit", sow: R2(5, 7), harvest: R2(7, 10) },
1375
+ { key: "shell_bean", fr: "haricots \xE0 \xE9cosser", en: "shelling beans", category: "fruit", sow: R2(5, 6), harvest: R2(8, 10) },
1376
+ { key: "pea", fr: "petits pois", en: "peas", category: "fruit", sow: [2, 3, 4, 10, 11], harvest: R2(5, 7) },
1377
+ { key: "broad_bean", fr: "f\xE8ves", en: "broad beans", category: "fruit", sow: [2, 3, 4, 10, 11], harvest: R2(5, 7) },
1378
+ { key: "pepper", fr: "poivrons", en: "peppers", category: "fruit", sowUnder: R2(2, 3), plant: [5], harvest: R2(7, 10) },
1379
+ { key: "eggplant", fr: "aubergines", en: "eggplants", category: "fruit", sowUnder: R2(2, 3), plant: [5], harvest: R2(7, 10) },
1380
+ { key: "chilli", fr: "piments", en: "chillies", category: "fruit", sowUnder: R2(2, 3), plant: [5], harvest: R2(8, 10) },
1381
+ { key: "melon", fr: "melons", en: "melons", category: "fruit", sowUnder: R2(3, 4), plant: [5], harvest: R2(7, 9) },
1382
+ { key: "sweetcorn", fr: "ma\xEFs doux", en: "sweetcorn", category: "fruit", sow: R2(4, 6), harvest: R2(8, 10) },
1383
+ { key: "strawberry", fr: "fraises", en: "strawberries", category: "fruit", plant: [3, 4, 8, 9], harvest: R2(5, 7) }
1384
+ ];
1385
+ var FLOWERS = [
1386
+ // Annuelles (semis)
1387
+ { key: "cosmos", fr: "cosmos", en: "cosmos", category: "flower", sow: R2(4, 6), harvest: R2(7, 10) },
1388
+ { key: "zinnia", fr: "zinnias", en: "zinnias", category: "flower", sowUnder: R2(3, 4), sow: [5], harvest: R2(7, 10) },
1389
+ { key: "calendula", fr: "soucis", en: "marigolds (calendula)", category: "flower", sow: R2(3, 6), harvest: R2(5, 10) },
1390
+ { key: "nasturtium", fr: "capucines", en: "nasturtiums", category: "flower", sow: R2(4, 6), harvest: R2(6, 10) },
1391
+ { key: "sunflower", fr: "tournesols", en: "sunflowers", category: "flower", sow: R2(4, 6), harvest: R2(7, 9) },
1392
+ { key: "tagetes", fr: "\u0153illets d'Inde", en: "French marigolds", category: "flower", sowUnder: R2(3, 4), plant: [5], harvest: R2(6, 10) },
1393
+ { key: "sweet_pea", fr: "pois de senteur", en: "sweet peas", category: "flower", sow: R2(2, 4), harvest: R2(6, 9) },
1394
+ { key: "cornflower", fr: "bleuets", en: "cornflowers", category: "flower", sow: [3, 4, 5, 9], harvest: R2(6, 8) },
1395
+ { key: "poppy", fr: "coquelicots", en: "poppies", category: "flower", sow: [3, 4, 5, 9], harvest: R2(5, 7) },
1396
+ { key: "nigella", fr: "nigelles", en: "love-in-a-mist", category: "flower", sow: [3, 4, 5, 9], harvest: R2(6, 8) },
1397
+ { key: "aster", fr: "reines-marguerites", en: "asters", category: "flower", sow: R2(4, 5), harvest: R2(8, 10) },
1398
+ // Bisannuelles
1399
+ { key: "hollyhock", fr: "roses tr\xE9mi\xE8res", en: "hollyhocks", category: "flower", sow: R2(5, 7), harvest: R2(6, 8) },
1400
+ { key: "foxglove", fr: "digitales", en: "foxgloves", category: "flower", sow: R2(5, 7), harvest: R2(6, 7) },
1401
+ { key: "forget_me_not", fr: "myosotis", en: "forget-me-nots", category: "flower", sow: R2(6, 7), harvest: R2(3, 5) },
1402
+ { key: "pansy", fr: "pens\xE9es", en: "pansies", category: "flower", sow: R2(6, 8), plant: [9, 10, 3], harvest: [3, 4, 5, 10, 11] },
1403
+ // Vivaces / bulbes (plantation)
1404
+ { key: "dahlia", fr: "dahlias", en: "dahlias", category: "flower", plant: R2(4, 5), harvest: R2(7, 10) },
1405
+ { key: "gladiolus", fr: "gla\xEFeuls", en: "gladioli", category: "flower", plant: R2(3, 6), harvest: R2(7, 9) },
1406
+ { key: "tulip", fr: "tulipes", en: "tulips", category: "flower", plant: R2(10, 12), harvest: R2(3, 5) },
1407
+ { key: "daffodil", fr: "narcisses", en: "daffodils", category: "flower", plant: R2(9, 11), harvest: R2(3, 4) },
1408
+ { key: "crocus", fr: "crocus", en: "crocuses", category: "flower", plant: R2(9, 11), harvest: R2(2, 3) },
1409
+ { key: "hyacinth", fr: "jacinthes", en: "hyacinths", category: "flower", plant: R2(9, 11), harvest: R2(3, 4) },
1410
+ { key: "lily", fr: "lys", en: "lilies", category: "flower", plant: R2(10, 4), harvest: R2(6, 8) },
1411
+ { key: "peony", fr: "pivoines", en: "peonies", category: "flower", plant: R2(9, 11), harvest: R2(5, 6) },
1412
+ { key: "iris", fr: "iris", en: "irises", category: "flower", plant: R2(7, 9), harvest: R2(5, 6) },
1413
+ { key: "lily_valley", fr: "muguet", en: "lily of the valley", category: "flower", plant: R2(10, 11), harvest: [5] },
1414
+ { key: "rose", fr: "rosiers", en: "roses", category: "flower", plant: [11, 12, 1, 2, 3], harvest: R2(5, 10) },
1415
+ { key: "lavender", fr: "lavande", en: "lavender", category: "flower", plant: [3, 4, 5, 9, 10], harvest: R2(6, 8) },
1416
+ { key: "chrysanthemum", fr: "chrysanth\xE8mes", en: "chrysanthemums", category: "flower", plant: R2(5, 6), harvest: R2(9, 11) }
1417
+ ];
1418
+ var PLANTING_CALENDAR = [...VEGETABLES, ...FLOWERS];
1419
+
1420
+ // ../ephemeris/src/dignities.ts
1421
+ var TABLE = {
1422
+ sun: { domicile: [4], exaltation: [0], detriment: [10], fall: [6] },
1423
+ moon: { domicile: [3], exaltation: [1], detriment: [9], fall: [7] },
1424
+ mercury: { domicile: [2, 5], exaltation: [5], detriment: [8, 11], fall: [11] },
1425
+ venus: { domicile: [1, 6], exaltation: [11], detriment: [0, 7], fall: [5] },
1426
+ mars: { domicile: [0, 7], exaltation: [9], detriment: [1, 6], fall: [3] },
1427
+ jupiter: { domicile: [8, 11], exaltation: [3], detriment: [2, 5], fall: [9] },
1428
+ saturn: { domicile: [9, 10], exaltation: [6], detriment: [3, 4], fall: [0] }
1429
+ };
1430
+ var LABELS = {
1431
+ domicile: { fr: "en domicile", en: "in domicile" },
1432
+ exaltation: { fr: "en exaltation", en: "exalted" },
1433
+ detriment: { fr: "en exil", en: "in detriment" },
1434
+ fall: { fr: "en chute", en: "in fall" }
1435
+ };
1436
+ var PRIORITY = ["domicile", "exaltation", "detriment", "fall"];
1437
+ function essentialDignity(planetKey, signIdx) {
1438
+ const key = typeof planetKey === "string" ? planetKey.toLowerCase() : "";
1439
+ const row = TABLE[key];
1440
+ if (!row || signIdx == null || !Number.isInteger(signIdx) || signIdx < 0 || signIdx > 11) {
1441
+ return null;
1442
+ }
1443
+ for (const kind of PRIORITY) {
1444
+ if (row[kind].includes(signIdx)) {
1445
+ return { kind, labelFr: LABELS[kind].fr, labelEn: LABELS[kind].en };
1446
+ }
1447
+ }
1448
+ return null;
1449
+ }
1450
+
1451
+ // ../ephemeris/src/natal-weight.ts
1452
+ var TRADITIONAL_RULER = [
1453
+ "mars",
1454
+ // 0 Bélier
1455
+ "venus",
1456
+ // 1 Taureau
1457
+ "mercury",
1458
+ // 2 Gémeaux
1459
+ "moon",
1460
+ // 3 Cancer
1461
+ "sun",
1462
+ // 4 Lion
1463
+ "mercury",
1464
+ // 5 Vierge
1465
+ "venus",
1466
+ // 6 Balance
1467
+ "mars",
1468
+ // 7 Scorpion
1469
+ "jupiter",
1470
+ // 8 Sagittaire
1471
+ "saturn",
1472
+ // 9 Capricorne
1473
+ "saturn",
1474
+ // 10 Verseau
1475
+ "jupiter"
1476
+ // 11 Poissons
1477
+ ];
1478
+ var W = {
1479
+ ascRuler: 0.45,
1480
+ // maître d'ascendant
1481
+ sunRuler: 0.22,
1482
+ // maître du signe solaire
1483
+ angular: 0.2,
1484
+ // en maison angulaire (1/4/7/10)
1485
+ conjAngle: 0.28,
1486
+ // à ≤ 5° de l'Ascendant ou du MC
1487
+ luminary: 0.16,
1488
+ // Soleil / Lune (base)
1489
+ tightAspectCore: 0.18,
1490
+ // aspect natal serré (<2°) impliquant un luminaire ou le maître
1491
+ tightAspect: 0.1,
1492
+ // aspect natal serré (<2°) sinon
1493
+ dignity: 0.1
1494
+ // domicile ou exaltation
1495
+ };
1496
+ var CONJ_ANGLE_ORB = 5;
1497
+ var TIGHT_ORB = 2;
1498
+ var TOP_THRESHOLD = 0.25;
1499
+ var TOP_MAX = 3;
1500
+ var LUMINARIES = /* @__PURE__ */ new Set(["sun", "moon"]);
1501
+ var ANGULAR_HOUSES = /* @__PURE__ */ new Set([1, 4, 7, 10]);
1502
+ function angularSep(a, b) {
1503
+ let d = Math.abs(a - b) % 360;
1504
+ if (d > 180) d = 360 - d;
1505
+ return d;
1506
+ }
1507
+ function signOf(p) {
1508
+ return p.signIdx ?? Math.floor((p.longitude % 360 + 360) % 360 / 30);
1509
+ }
1510
+ function computeNatalWeight(chart) {
1511
+ const timeKnown = chart.birthTimeKnown ?? true;
1512
+ const planets = chart.planets ?? {};
1513
+ const acc = {};
1514
+ const add = (planet, delta, role) => {
1515
+ if (!planet || !planets[planet]) return;
1516
+ const e = acc[planet] ?? (acc[planet] = { weight: 0, roles: [] });
1517
+ e.weight += delta;
1518
+ if (!e.roles.some((r) => r.key === role.key)) e.roles.push(role);
1519
+ };
1520
+ let chartRuler = null;
1521
+ if (timeKnown && typeof chart.asc === "number") {
1522
+ const ascSign = Math.floor((chart.asc % 360 + 360) % 360 / 30);
1523
+ chartRuler = TRADITIONAL_RULER[ascSign] ?? null;
1524
+ if (chartRuler) {
1525
+ add(chartRuler, W.ascRuler, { key: "ascRuler", labelFr: "ma\xEEtre d'ascendant", labelEn: "chart ruler" });
1526
+ }
1527
+ }
1528
+ const sun = planets["sun"];
1529
+ if (sun) {
1530
+ const sunRuler = TRADITIONAL_RULER[signOf(sun)];
1531
+ if (sunRuler) {
1532
+ add(sunRuler, W.sunRuler, { key: "sunRuler", labelFr: "ma\xEEtre du Soleil", labelEn: "ruler of the Sun" });
1533
+ }
1534
+ }
1535
+ for (const lum of LUMINARIES) {
1536
+ if (planets[lum]) add(lum, W.luminary, { key: "luminary", labelFr: "luminaire", labelEn: "luminary" });
1537
+ }
1538
+ if (timeKnown) {
1539
+ for (const [key, p] of Object.entries(planets)) {
1540
+ if (!p || typeof p.longitude !== "number") continue;
1541
+ if (p.house != null && ANGULAR_HOUSES.has(p.house)) {
1542
+ add(key, W.angular, { key: "angular", labelFr: "angulaire", labelEn: "angular" });
1543
+ }
1544
+ if (typeof chart.asc === "number" && angularSep(p.longitude, chart.asc) <= CONJ_ANGLE_ORB) {
1545
+ add(key, W.conjAngle, { key: "conjAsc", labelFr: "conjoint \xE0 l'Ascendant", labelEn: "conjunct the Ascendant" });
1546
+ }
1547
+ if (typeof chart.mc === "number" && angularSep(p.longitude, chart.mc) <= CONJ_ANGLE_ORB) {
1548
+ add(key, W.conjAngle, { key: "conjMc", labelFr: "conjoint au Milieu du Ciel", labelEn: "conjunct the Midheaven" });
1549
+ }
1550
+ }
1551
+ }
1552
+ for (const asp of chart.aspects ?? []) {
1553
+ if (typeof asp.orb !== "number" || asp.orb >= TIGHT_ORB) continue;
1554
+ const a = asp.planet1 ?? asp.p1;
1555
+ const b = asp.planet2 ?? asp.p2;
1556
+ if (!a || !b) continue;
1557
+ const core = LUMINARIES.has(a) || LUMINARIES.has(b) || a === chartRuler || b === chartRuler;
1558
+ const delta = core ? W.tightAspectCore : W.tightAspect;
1559
+ const role = { key: "tightAspect", labelFr: "aspect natal serr\xE9", labelEn: "tight natal aspect" };
1560
+ add(a, delta, role);
1561
+ add(b, delta, role);
1562
+ }
1563
+ for (const [key, p] of Object.entries(planets)) {
1564
+ if (!p || typeof p.longitude !== "number") continue;
1565
+ const dig = essentialDignity(key, signOf(p));
1566
+ if (dig && (dig.kind === "domicile" || dig.kind === "exaltation")) {
1567
+ add(key, W.dignity, { key: "dignity", labelFr: dig.labelFr, labelEn: dig.labelEn });
1568
+ }
1569
+ }
1570
+ const byPlanet = {};
1571
+ for (const [planet, e] of Object.entries(acc)) {
1572
+ byPlanet[planet] = { planet, weight: Math.min(1, Math.round(e.weight * 100) / 100), roles: e.roles };
1573
+ }
1574
+ const top = Object.values(byPlanet).filter((e) => e.weight >= TOP_THRESHOLD).sort((x, y) => y.weight - x.weight).slice(0, TOP_MAX);
1575
+ return { byPlanet, top, chartRuler };
1576
+ }
1577
+
1578
+ // ../ephemeris/src/natal-patterns.ts
1579
+ var MAJORS = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"];
1580
+ var ORB = {
1581
+ grandTrine: 8,
1582
+ square: 7,
1583
+ // T-carré / grande croix
1584
+ opposition: 7,
1585
+ yodSextile: 4,
1586
+ yodQuincunx: 3
1587
+ };
1588
+ var STELLIUM_MIN = 3;
1589
+ function sep(a, b) {
1590
+ let d = Math.abs(a - b) % 360;
1591
+ if (d > 180) d = 360 - d;
1592
+ return d;
1593
+ }
1594
+ function isAspect(a, b, angle, orb) {
1595
+ return Math.abs(sep(a, b) - angle) <= orb;
1596
+ }
1597
+ function signOf2(p) {
1598
+ return p.signIdx ?? Math.floor((p.longitude % 360 + 360) % 360 / 30);
1599
+ }
1600
+ var ELEMENT_FR = ["Feu", "Terre", "Air", "Eau"];
1601
+ var ELEMENT_EN = ["Fire", "Earth", "Air", "Water"];
1602
+ function detectNatalPatterns(input) {
1603
+ const timeKnown = input.birthTimeKnown ?? true;
1604
+ const bodies = MAJORS.filter((k) => {
1605
+ const p = input.planets?.[k];
1606
+ return p && typeof p.longitude === "number";
1607
+ });
1608
+ const lon = (k) => input.planets[k].longitude;
1609
+ const out = [];
1610
+ const bySign = /* @__PURE__ */ new Map();
1611
+ for (const k of bodies) {
1612
+ const s = signOf2(input.planets[k]);
1613
+ (bySign.get(s) ?? bySign.set(s, []).get(s)).push(k);
1614
+ }
1615
+ for (const [s, members] of bySign) {
1616
+ if (members.length >= STELLIUM_MIN) {
1617
+ out.push({
1618
+ kind: "stellium",
1619
+ by: "sign",
1620
+ signIdx: s,
1621
+ planets: members,
1622
+ typeFr: "stellium",
1623
+ typeEn: "stellium"
1624
+ });
1625
+ }
1626
+ }
1627
+ if (timeKnown) {
1628
+ const byHouse = /* @__PURE__ */ new Map();
1629
+ for (const k of bodies) {
1630
+ const h = input.planets[k].house;
1631
+ if (h != null) (byHouse.get(h) ?? byHouse.set(h, []).get(h)).push(k);
1632
+ }
1633
+ for (const [h, members] of byHouse) {
1634
+ if (members.length >= STELLIUM_MIN) {
1635
+ const sameAsSign = out.some((p) => p.kind === "stellium" && p.by === "sign" && p.planets.length === members.length && members.every((m) => p.planets.includes(m)));
1636
+ if (!sameAsSign) {
1637
+ out.push({
1638
+ kind: "stellium",
1639
+ by: "house",
1640
+ house: h,
1641
+ planets: members,
1642
+ typeFr: "stellium",
1643
+ typeEn: "stellium"
1644
+ });
1645
+ }
1646
+ }
1647
+ }
1648
+ }
1649
+ const grandTrines = [];
1650
+ for (let i = 0; i < bodies.length; i++)
1651
+ for (let j = i + 1; j < bodies.length; j++)
1652
+ for (let k = j + 1; k < bodies.length; k++) {
1653
+ const [a, b, c] = [bodies[i], bodies[j], bodies[k]];
1654
+ if (isAspect(lon(a), lon(b), 120, ORB.grandTrine) && isAspect(lon(b), lon(c), 120, ORB.grandTrine) && isAspect(lon(a), lon(c), 120, ORB.grandTrine)) {
1655
+ const els = [a, b, c].map((x) => signOf2(input.planets[x]) % 4);
1656
+ const element = els.every((e) => e === els[0]) ? els[0] : void 0;
1657
+ grandTrines.push([a, b, c]);
1658
+ out.push({
1659
+ kind: "grandTrine",
1660
+ planets: [a, b, c],
1661
+ element,
1662
+ typeFr: element != null ? `grand trigone de ${ELEMENT_FR[element]}` : "grand trigone",
1663
+ typeEn: element != null ? `grand trine in ${ELEMENT_EN[element]}` : "grand trine"
1664
+ });
1665
+ }
1666
+ }
1667
+ const opps = [];
1668
+ for (let i = 0; i < bodies.length; i++)
1669
+ for (let j = i + 1; j < bodies.length; j++)
1670
+ if (isAspect(lon(bodies[i]), lon(bodies[j]), 180, ORB.opposition))
1671
+ opps.push([bodies[i], bodies[j]]);
1672
+ const squares = (x, y) => isAspect(lon(x), lon(y), 90, ORB.square);
1673
+ const crossSets = [];
1674
+ for (let i = 0; i < opps.length; i++)
1675
+ for (let j = i + 1; j < opps.length; j++) {
1676
+ const [a, b] = opps[i], [c, d] = opps[j];
1677
+ const four = [a, b, c, d];
1678
+ if (new Set(four).size !== 4) continue;
1679
+ if (squares(a, c) && squares(a, d) && squares(b, c) && squares(b, d)) {
1680
+ crossSets.push(new Set(four));
1681
+ out.push({ kind: "grandCross", planets: four, typeFr: "grande croix", typeEn: "grand cross" });
1682
+ }
1683
+ }
1684
+ for (const [a, b] of opps) {
1685
+ for (const c of bodies) {
1686
+ if (c === a || c === b) continue;
1687
+ if (squares(a, c) && squares(b, c)) {
1688
+ if (crossSets.some((s) => s.has(a) && s.has(b) && s.has(c))) continue;
1689
+ out.push({ kind: "tSquare", planets: [a, b, c], apex: c, typeFr: "T-carr\xE9", typeEn: "T-square" });
1690
+ }
1691
+ }
1692
+ }
1693
+ for (let i = 0; i < bodies.length; i++)
1694
+ for (let j = i + 1; j < bodies.length; j++) {
1695
+ const a = bodies[i], b = bodies[j];
1696
+ if (!isAspect(lon(a), lon(b), 60, ORB.yodSextile)) continue;
1697
+ for (const c of bodies) {
1698
+ if (c === a || c === b) continue;
1699
+ if (isAspect(lon(a), lon(c), 150, ORB.yodQuincunx) && isAspect(lon(b), lon(c), 150, ORB.yodQuincunx)) {
1700
+ out.push({ kind: "yod", planets: [a, b, c], apex: c, typeFr: "yod", typeEn: "yod" });
1701
+ }
1702
+ }
1703
+ }
1704
+ return out;
1705
+ }
1706
+
1707
+ // ../ephemeris/src/planetary-ages.ts
1708
+ var AGE_BANDS = [
1709
+ { maxAge: 7, planetKey: "moon", fr: "l'enfance, le lien affectif et le besoin de s\xE9curit\xE9", en: "childhood, emotional bonding and the need for security" },
1710
+ { maxAge: 14, planetKey: "mercury", fr: "l'apprentissage, la curiosit\xE9 et les premiers \xE9changes", en: "learning, curiosity and first exchanges" },
1711
+ { maxAge: 24, planetKey: "venus", fr: "l'amour, la sensualit\xE9 et le go\xFBt de plaire", en: "love, sensuality and the taste for pleasing" },
1712
+ { maxAge: 36, planetKey: "mars", fr: "l'affirmation de soi, la conqu\xEAte et l'action", en: "self-assertion, conquest and action" },
1713
+ { maxAge: 48, planetKey: "sun", fr: "la r\xE9ussite, le rayonnement et l'autorit\xE9", en: "achievement, radiance and authority" },
1714
+ { maxAge: 63, planetKey: "jupiter", fr: "le d\xE9ploiement, la transmission et l'expansion", en: "growth, transmission and expansion" },
1715
+ { maxAge: Number.POSITIVE_INFINITY, planetKey: "saturn", fr: "le bilan, la sagesse et l'essentiel", en: "reckoning, wisdom and the essential" }
1716
+ ];
1717
+
1718
+ // ../ephemeris/src/birth-lunation.ts
1719
+ var LUNATION_TYPES = [
1720
+ { key: "new", nameFr: "Nouvelle Lune", nameEn: "New Moon", glossFr: "instinct, \xE9lan spontan\xE9, commencement subjectif", glossEn: "instinct, spontaneous drive, subjective beginnings" },
1721
+ { key: "crescent", nameFr: "Croissant", nameEn: "Crescent", glossFr: "l'effort d'\xE9merger, s'arracher au pass\xE9, volont\xE9 d'avancer", glossEn: "the effort to emerge, breaking from the past, will to advance" },
1722
+ { key: "first_quarter", nameFr: "Premier Quartier", nameEn: "First Quarter", glossFr: "crise d'action, construire, bousculer les r\xE9sistances", glossEn: "crisis of action, building, pushing through resistance" },
1723
+ { key: "gibbous", nameFr: "Gibbeuse", nameEn: "Gibbous", glossFr: "perfectionnement, qu\xEAte de sens, analyse, d\xE9vouement", glossEn: "refinement, search for meaning, analysis, devotion" },
1724
+ { key: "full", nameFr: "Pleine Lune", nameEn: "Full Moon", glossFr: "pleine conscience, relation, objectivit\xE9, r\xE9v\xE9lation", glossEn: "full awareness, relationship, objectivity, revelation" },
1725
+ { key: "disseminating", nameFr: "Diffusante", nameEn: "Disseminating", glossFr: "transmettre, partager, enseigner ce qu'on a compris", glossEn: "transmitting, sharing, teaching what one has grasped" },
1726
+ { key: "last_quarter", nameFr: "Dernier Quartier", nameEn: "Last Quarter", glossFr: "crise de conscience, r\xE9orientation, remise en question", glossEn: "crisis of consciousness, reorientation, questioning" },
1727
+ { key: "balsamic", nameFr: "Balsamique", nameEn: "Balsamic", glossFr: "l\xE2cher-prise, transition, graine du futur, retrait", glossEn: "release, transition, seed of the future, withdrawal" }
1728
+ ];
1729
+ function lunationType(sunLon, moonLon) {
1730
+ if (!Number.isFinite(sunLon) || !Number.isFinite(moonLon)) return null;
1731
+ const elong = ((moonLon - sunLon) % 360 + 360) % 360;
1732
+ const idx = Math.min(7, Math.floor(elong / 45));
1733
+ return LUNATION_TYPES[idx];
1734
+ }
1735
+
1080
1736
  // ../ephemeris/src/time-utc.service.ts
1081
1737
  import { DateTime } from "luxon";
1082
1738
  var TimezoneError = class extends Error {
@@ -1274,8 +1930,27 @@ function enrich(result, meta) {
1274
1930
  symbol: a.symbol,
1275
1931
  tone: a.tone
1276
1932
  }));
1277
- return {
1933
+ const planets = {};
1934
+ for (const [key, p] of Object.entries(result.planets)) {
1935
+ const signIdx = p.signIdx ?? Math.floor((p.longitude % 360 + 360) % 360 / 30);
1936
+ planets[key] = { ...p, dignity: essentialDignity(key, signIdx) };
1937
+ }
1938
+ const sunLon = result.planets?.sun?.longitude;
1939
+ const moonLon = result.planets?.moon?.longitude;
1940
+ const lunation = typeof sunLon === "number" && typeof moonLon === "number" ? lunationType(sunLon, moonLon) : null;
1941
+ const weight = computeNatalWeight({
1942
+ planets: result.planets,
1943
+ asc: result.houses.asc ?? null,
1944
+ mc: result.houses.mc ?? null,
1945
+ aspects: result.aspects,
1946
+ birthTimeKnown: meta.birthTimeKnown
1947
+ });
1948
+ const patterns = detectNatalPatterns({
1278
1949
  planets: result.planets,
1950
+ birthTimeKnown: meta.birthTimeKnown
1951
+ });
1952
+ return {
1953
+ planets,
1279
1954
  houses,
1280
1955
  asc: result.houses.asc,
1281
1956
  mc: result.houses.mc,
@@ -1288,6 +1963,12 @@ function enrich(result, meta) {
1288
1963
  moonPhase: result.moonPhase,
1289
1964
  numerology: meta.localBirthDate ? computeLifePath(meta.localBirthDate) : result.numerology,
1290
1965
  // STAB-PRE-5-V1
1966
+ lunation,
1967
+ // NATAL-EXPOSE-V1
1968
+ weight,
1969
+ // NATAL-EXPOSE-V1
1970
+ patterns,
1971
+ // NATAL-EXPOSE-V1
1291
1972
  source: result.source,
1292
1973
  zodiac: result.zodiac,
1293
1974
  houseSystem: result.houseSystem,
@@ -1312,7 +1993,7 @@ function chartCacheKey(args) {
1312
1993
  e: getActiveEngine()
1313
1994
  });
1314
1995
  const hash = createHash("sha256").update(payload).digest("hex").slice(0, 16);
1315
- return `chart:v7:${args.natalId}:${hash}`;
1996
+ return `chart:v8:${args.natalId}:${hash}`;
1316
1997
  }
1317
1998
  var EphemerisService = class {
1318
1999
  /**
@@ -1413,6 +2094,33 @@ var EphemerisService = class {
1413
2094
  longitude: city.lng
1414
2095
  });
1415
2096
  }
2097
+ // ──────────────────────────────────────────────────────
2098
+ // SYNASTRY-DECAN-V1
2099
+ // Thème « décan » : positions planétaires calculées à midi UTC
2100
+ // (ou à l'heure fournie) SANS lieu de naissance. Les longitudes
2101
+ // écliptiques géocentriques ne dépendent que du temps (JD), pas
2102
+ // de lat/lon — seuls les maisons/ASC/MC en dépendent, et on les
2103
+ // ignore en aval (mode décan : Soleil + planètes lentes only).
2104
+ // Utilisé en synastrie quand un partenaire n'a ni heure ni ville.
2105
+ // ──────────────────────────────────────────────────────
2106
+ /**
2107
+ * @param birthDate Date de naissance locale (YYYY-MM-DD).
2108
+ * @param birthTime Heure locale (HH:MM). Défaut 12:00 si absente.
2109
+ * @param birthTimeKnown Propagé dans meta pour le hedging IA.
2110
+ */
2111
+ async calculateDecanChart(args) {
2112
+ const localBirthTime = args.birthTime ?? "12:00";
2113
+ const key = createHash("sha256").update(`decan:${args.birthDate}:${localBirthTime}:${args.birthTimeKnown}`).digest("hex").slice(0, 12);
2114
+ return this.calculateNatalChart({
2115
+ natalId: `decan-${key}`,
2116
+ localBirthDate: args.birthDate,
2117
+ localBirthTime,
2118
+ ianaTz: "UTC",
2119
+ latitude: 0,
2120
+ longitude: 0,
2121
+ birthTimeKnown: args.birthTimeKnown
2122
+ });
2123
+ }
1416
2124
  /** Ciel du moment présent (transits) pour une position. */
1417
2125
  async getCurrentSky(latitude, longitude, opts = {}) {
1418
2126
  const result = computeCurrentSky2(latitude, longitude, opts);
@@ -1427,6 +2135,30 @@ var EphemerisService = class {
1427
2135
  utcISO: nowIso
1428
2136
  });
1429
2137
  }
2138
+ /**
2139
+ * Ciel à un instant précis (JD UT) pour une position — variante déterministe
2140
+ * de `getCurrentSky`.
2141
+ *
2142
+ * HOROSCOPE-CHIP-COHERENCE-V1 : sert à ancrer les aspects transit→natal qui
2143
+ * pilotent les « pastilles d'énergies » et la prose IA sur un MÊME instant
2144
+ * stable du jour (midi UTC), au lieu du « maintenant » horaire. Sans cet
2145
+ * ancrage, la prose (cachée au premier chargement) et les puces (recalculées
2146
+ * en direct à chaque requête) divergeaient dès qu'un corps rapide (Lune,
2147
+ * astéroïde) franchissait un seuil d'orbe dans la journée.
2148
+ */
2149
+ async getSkyAt(JD, latitude, longitude, opts = {}) {
2150
+ const result = computeChartFromJD2(JD, latitude, longitude, opts);
2151
+ const iso = new Date((JD - 24405875e-1) * 864e5).toISOString();
2152
+ return enrich(result, {
2153
+ localBirthDate: iso.slice(0, 10),
2154
+ localBirthTime: iso.slice(11, 16),
2155
+ ianaTz: "UTC",
2156
+ offsetMinutes: 0,
2157
+ resolution: "valid",
2158
+ birthTimeKnown: true,
2159
+ utcISO: iso
2160
+ });
2161
+ }
1430
2162
  // ──────────────────────────────────────────────────────
1431
2163
  // LEGACY — conservé pour compat pendant la migration
1432
2164
  // ──────────────────────────────────────────────────────
@@ -1478,9 +2210,7 @@ var EphemerisService = class {
1478
2210
  };
1479
2211
  var ephemerisService = new EphemerisService();
1480
2212
 
1481
- // src/index.ts
1482
- process.env["EPHEMERIS_DISABLE_REDIS"] ??= "1";
1483
- process.env["ASTRO_ALLOW_FALLBACK"] ??= "true";
2213
+ // src/astro-tools.ts
1484
2214
  var SIGNS_FR = [
1485
2215
  "B\xE9lier",
1486
2216
  "Taureau",
@@ -1495,9 +2225,160 @@ var SIGNS_FR = [
1495
2225
  "Verseau",
1496
2226
  "Poissons"
1497
2227
  ];
1498
- function jdFromDate(d) {
1499
- return d.getTime() / 864e5 + 24405875e-1;
2228
+ var JD_UNIX_EPOCH = 24405875e-1;
2229
+ var MS_PER_DAY = 864e5;
2230
+ var JULIAN_YEAR = 365.25;
2231
+ function jdToDate(jd2) {
2232
+ return new Date((jd2 - JD_UNIX_EPOCH) * MS_PER_DAY);
2233
+ }
2234
+ function dateToJd(d) {
2235
+ return d.getTime() / MS_PER_DAY + JD_UNIX_EPOCH;
2236
+ }
2237
+ function n3602(x) {
2238
+ return (x % 360 + 360) % 360;
2239
+ }
2240
+ function wrap1802(x) {
2241
+ return ((x + 180) % 360 + 360) % 360 - 180;
2242
+ }
2243
+ function angularSep2(a, b) {
2244
+ return Math.abs(wrap1802(a - b));
2245
+ }
2246
+ function signIdxOf(lon) {
2247
+ return Math.floor(n3602(lon) / 30) % 12;
2248
+ }
2249
+ function circularMidpoint(a, b) {
2250
+ return n3602(a + wrap1802(b - a) / 2);
2251
+ }
2252
+ function bodyLongitude(key, jd2) {
2253
+ return allPositions2(jd2)[key]?.longitude;
2254
+ }
2255
+ var NOISE_KEYS = /* @__PURE__ */ new Set([
2256
+ "fortune",
2257
+ "southNode",
2258
+ "ceres",
2259
+ "pallas",
2260
+ "juno",
2261
+ "vesta",
2262
+ "lilithTrue"
2263
+ ]);
2264
+ function crossAspects(left, right) {
2265
+ const out = [];
2266
+ for (const [lk, lp] of Object.entries(left)) {
2267
+ if (NOISE_KEYS.has(lk)) continue;
2268
+ for (const [rk, rp] of Object.entries(right)) {
2269
+ if (NOISE_KEYS.has(rk)) continue;
2270
+ const sep2 = angularSep2(lp.longitude, rp.longitude);
2271
+ for (const a of ASPECT_TYPES) {
2272
+ const isLum = lk === "sun" || lk === "moon" || rk === "sun" || rk === "moon";
2273
+ const maxOrb = a.orb + (isLum ? 2 : 0);
2274
+ const o = Math.abs(sep2 - a.angle);
2275
+ if (o <= maxOrb) {
2276
+ out.push({
2277
+ a: lk,
2278
+ b: rk,
2279
+ type: a.type,
2280
+ typeFr: a.nameFr,
2281
+ symbol: a.symbol,
2282
+ tone: a.tone,
2283
+ orb: Number(o.toFixed(2)),
2284
+ exact: o < 1
2285
+ });
2286
+ }
2287
+ }
2288
+ }
2289
+ }
2290
+ return out.sort((x, y) => x.orb - y.orb);
2291
+ }
2292
+ function toneSummary(aspects) {
2293
+ let harmoniques = 0, tensions = 0, neutres = 0;
2294
+ for (const a of aspects) {
2295
+ if (a.tone === "h") harmoniques++;
2296
+ else if (a.tone === "t") tensions++;
2297
+ else neutres++;
2298
+ }
2299
+ return { total: aspects.length, harmoniques, tensions, neutres };
1500
2300
  }
2301
+ function compositePositions(a, b) {
2302
+ const out = [];
2303
+ for (const key of Object.keys(a)) {
2304
+ if (NOISE_KEYS.has(key)) continue;
2305
+ const bp = b[key];
2306
+ if (!bp) continue;
2307
+ const lon = circularMidpoint(a[key].longitude, bp.longitude);
2308
+ out.push({ corps: key, longitude: Number(lon.toFixed(4)), signIdx: signIdxOf(lon) });
2309
+ }
2310
+ return out;
2311
+ }
2312
+ var MAX_ITERS = 42;
2313
+ function bisectZero(f, j0, j1) {
2314
+ let lo = j0, hi = j1;
2315
+ for (let i = 0; i < MAX_ITERS; i++) {
2316
+ const mid = (lo + hi) / 2;
2317
+ if (f(mid) <= 0) lo = mid;
2318
+ else hi = mid;
2319
+ }
2320
+ return (lo + hi) / 2;
2321
+ }
2322
+ function bisectFlip(value, j0, j1) {
2323
+ const v0 = value(j0);
2324
+ let lo = j0, hi = j1;
2325
+ for (let i = 0; i < MAX_ITERS; i++) {
2326
+ const mid = (lo + hi) / 2;
2327
+ if (value(mid) === v0) lo = mid;
2328
+ else hi = mid;
2329
+ }
2330
+ return (lo + hi) / 2;
2331
+ }
2332
+ function findLongitudeReturn(key, target, jdStart, jdEnd, step) {
2333
+ const f = (jd2) => wrap1802((bodyLongitude(key, jd2) ?? NaN) - target);
2334
+ let prev = jdStart;
2335
+ let fprev = f(prev);
2336
+ for (let jd2 = jdStart + step; jd2 <= jdEnd + 1e-9; jd2 += step) {
2337
+ const fcur = f(jd2);
2338
+ if (fprev <= 0 && fcur > 0 && fcur - fprev < 180) {
2339
+ return bisectZero(f, prev, jd2);
2340
+ }
2341
+ prev = jd2;
2342
+ fprev = fcur;
2343
+ }
2344
+ return null;
2345
+ }
2346
+ function findIngresses(key, jdStart, jdEnd, step) {
2347
+ const sIdx = (jd2) => signIdxOf(bodyLongitude(key, jd2) ?? NaN);
2348
+ const out = [];
2349
+ let prev = jdStart;
2350
+ let sPrev = sIdx(prev);
2351
+ for (let jd2 = jdStart + step; jd2 <= jdEnd + 1e-9; jd2 += step) {
2352
+ const sCur = sIdx(jd2);
2353
+ if (sCur !== sPrev) {
2354
+ const tJd = bisectFlip(sIdx, prev, jd2);
2355
+ out.push({ jd: tJd, deSigneIdx: sPrev, versSigneIdx: sIdx(tJd + 1e-6) });
2356
+ }
2357
+ prev = jd2;
2358
+ sPrev = sCur;
2359
+ }
2360
+ return out;
2361
+ }
2362
+ function findRetrogradeStations(key, jdStart, jdEnd, step) {
2363
+ const retro = (jd2) => isRetrograde3(key, jd2);
2364
+ const out = [];
2365
+ let prev = jdStart;
2366
+ let rPrev = retro(prev);
2367
+ for (let jd2 = jdStart + step; jd2 <= jdEnd + 1e-9; jd2 += step) {
2368
+ const rCur = retro(jd2);
2369
+ if (rCur !== rPrev) {
2370
+ const tJd = bisectFlip(retro, prev, jd2);
2371
+ out.push({ jd: tJd, station: retro(tJd + 1e-6) ? "retrograde" : "direct" });
2372
+ }
2373
+ prev = jd2;
2374
+ rPrev = rCur;
2375
+ }
2376
+ return out;
2377
+ }
2378
+
2379
+ // src/index.ts
2380
+ process.env["EPHEMERIS_DISABLE_REDIS"] ??= "1";
2381
+ process.env["ASTRO_ALLOW_FALLBACK"] ??= "true";
1501
2382
  function parseUtc(datetime) {
1502
2383
  const d = new Date(datetime);
1503
2384
  if (Number.isNaN(d.getTime())) {
@@ -1505,6 +2386,9 @@ function parseUtc(datetime) {
1505
2386
  }
1506
2387
  return d;
1507
2388
  }
2389
+ function signOf3(lon) {
2390
+ return SIGNS_FR[signIdxOf(lon)] ?? null;
2391
+ }
1508
2392
  function formatPlanets(positions) {
1509
2393
  return Object.values(positions).map((p) => ({
1510
2394
  corps: p.key,
@@ -1518,9 +2402,38 @@ function formatPlanets(positions) {
1518
2402
  function jsonResult(value) {
1519
2403
  return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
1520
2404
  }
2405
+ var BIRTH_SHAPE = {
2406
+ date: z.string().describe('Date de naissance locale, format YYYY-MM-DD, ex. "1990-05-21".'),
2407
+ time: z.string().describe('Heure de naissance locale, format HH:mm (24 h), ex. "14:30".'),
2408
+ timezone: z.string().describe('Fuseau IANA du lieu de naissance, ex. "Europe/Paris".'),
2409
+ latitude: z.number().min(-90).max(90).describe("Latitude en degr\xE9s d\xE9cimaux (positif = Nord)."),
2410
+ longitude: z.number().min(-180).max(180).describe("Longitude en degr\xE9s d\xE9cimaux (positif = Est).")
2411
+ };
2412
+ var BODY_ENUM = z.enum([
2413
+ "sun",
2414
+ "moon",
2415
+ "mercury",
2416
+ "venus",
2417
+ "mars",
2418
+ "jupiter",
2419
+ "saturn",
2420
+ "uranus",
2421
+ "neptune",
2422
+ "pluto"
2423
+ ]);
2424
+ var RETRO_BODY_ENUM = z.enum([
2425
+ "mercury",
2426
+ "venus",
2427
+ "mars",
2428
+ "jupiter",
2429
+ "saturn",
2430
+ "uranus",
2431
+ "neptune",
2432
+ "pluto"
2433
+ ]);
1521
2434
  var server = new McpServer({
1522
2435
  name: "llmastro-ephemeris",
1523
- version: "0.0.1"
2436
+ version: "0.2.0"
1524
2437
  });
1525
2438
  server.tool(
1526
2439
  "get_planet_positions",
@@ -1529,13 +2442,12 @@ server.tool(
1529
2442
  datetime: z.string().describe('Instant en ISO 8601 UTC, ex. "2026-07-18T14:00:00Z". Sans fuseau, interpr\xE9t\xE9 en UTC.')
1530
2443
  },
1531
2444
  async ({ datetime }) => {
1532
- const jd2 = jdFromDate(parseUtc(datetime));
1533
- const positions = allPositions2(jd2);
2445
+ const jd2 = dateToJd(parseUtc(datetime));
1534
2446
  return jsonResult({
1535
2447
  datetimeUTC: parseUtc(datetime).toISOString(),
1536
2448
  jd: jd2,
1537
2449
  moteur: getActiveEngine(),
1538
- corps: formatPlanets(positions)
2450
+ corps: formatPlanets(allPositions2(jd2))
1539
2451
  });
1540
2452
  }
1541
2453
  );
@@ -1543,10 +2455,10 @@ server.tool(
1543
2455
  "get_moon_phase",
1544
2456
  "Phase de la Lune (nom, illumination %, description) \xE0 un instant donn\xE9, d\xE9riv\xE9e de la position r\xE9elle Soleil/Lune.",
1545
2457
  {
1546
- datetime: z.string().describe(`Instant en ISO 8601 UTC, ex. "2026-07-18T14:00:00Z". D\xE9faut d'interpr\xE9tation : UTC.`)
2458
+ datetime: z.string().describe('Instant en ISO 8601 UTC, ex. "2026-07-18T14:00:00Z".')
1547
2459
  },
1548
2460
  async ({ datetime }) => {
1549
- const jd2 = jdFromDate(parseUtc(datetime));
2461
+ const jd2 = dateToJd(parseUtc(datetime));
1550
2462
  const mp = moonPhase2(jd2);
1551
2463
  return jsonResult({
1552
2464
  datetimeUTC: parseUtc(datetime).toISOString(),
@@ -1560,76 +2472,347 @@ server.tool(
1560
2472
  }
1561
2473
  );
1562
2474
  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.",
2475
+ "get_current_sky",
2476
+ "Le ciel \xE0 l'instant pr\xE9sent (transits du moment) pour un lieu donn\xE9 : positions en maisons + aspects.",
1565
2477
  {
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
2478
  latitude: z.number().min(-90).max(90).describe("Latitude en degr\xE9s d\xE9cimaux (positif = Nord)."),
1570
2479
  longitude: z.number().min(-180).max(180).describe("Longitude en degr\xE9s d\xE9cimaux (positif = Est).")
1571
2480
  },
1572
- async ({ date, time, timezone, latitude, longitude }) => {
2481
+ async ({ latitude, longitude }) => {
2482
+ const chart = computeCurrentSky2(latitude, longitude);
2483
+ return jsonResult({
2484
+ instant: (/* @__PURE__ */ new Date()).toISOString(),
2485
+ jd: chart.JD,
2486
+ moteur: getActiveEngine(),
2487
+ retrogrades: chart.retrogrades,
2488
+ phaseLunaire: { phase: chart.moonPhase.phase, illumination: chart.moonPhase.illumination },
2489
+ corps: formatPlanets(chart.planets),
2490
+ aspects: chart.aspects
2491
+ });
2492
+ }
2493
+ );
2494
+ server.tool(
2495
+ "get_aspects",
2496
+ "Aspects (majeurs et mineurs) entre les plan\xE8tes \xE0 un instant donn\xE9, avec orbe et tonalit\xE9. Grille d'aspects calcul\xE9e par le moteur, pas estim\xE9e.",
2497
+ {
2498
+ datetime: z.string().describe('Instant en ISO 8601 UTC, ex. "2026-07-18T14:00:00Z".')
2499
+ },
2500
+ async ({ datetime }) => {
2501
+ const jd2 = dateToJd(parseUtc(datetime));
2502
+ return jsonResult({
2503
+ datetimeUTC: parseUtc(datetime).toISOString(),
2504
+ jd: jd2,
2505
+ moteur: getActiveEngine(),
2506
+ aspects: calculateAspects(allPositions2(jd2))
2507
+ });
2508
+ }
2509
+ );
2510
+ server.tool(
2511
+ "get_natal_chart",
2512
+ "Th\xE8me natal complet : positions en maisons, aspects, ASC/MC, phase lunaire, Lots herm\xE9tiques. Prend une date/heure LOCALE + fuseau IANA + coordonn\xE9es. Zodiaque tropical (d\xE9faut) ou sid\xE9ral (Lahiri), et syst\xE8me de maisons au choix (Placidus par d\xE9faut).",
2513
+ {
2514
+ ...BIRTH_SHAPE,
2515
+ zodiac: z.enum(["tropical", "sidereal"]).optional().describe('Zodiaque : "tropical" (d\xE9faut, occidental) ou "sidereal" (ayanamsa Lahiri).'),
2516
+ houseSystem: z.enum(["placidus", "equal", "whole_sign", "koch", "porphyry", "regiomontanus", "campanus"]).optional().describe('Syst\xE8me de maisons (d\xE9faut "placidus"). Regiomontanus/Campanus/Koch exigent le moteur swisseph ; sinon repli Placidus signal\xE9 par le champ systemeMaisons retourn\xE9.')
2517
+ },
2518
+ async ({ date, time, timezone, latitude, longitude, zodiac, houseSystem }) => {
1573
2519
  const { jdUT, utcISO, offsetMinutes } = localToUTC(date, time, timezone);
1574
- const chart = computeChartFromJD2(jdUT, latitude, longitude);
2520
+ const opts = {};
2521
+ if (zodiac) opts.zodiac = zodiac;
2522
+ if (houseSystem) opts.houseSystem = houseSystem;
2523
+ const chart = computeChartFromJD2(jdUT, latitude, longitude, opts);
1575
2524
  return jsonResult({
1576
2525
  naissance: { date, time, timezone, latitude, longitude },
1577
2526
  utcISO,
1578
2527
  offsetMinutes,
1579
2528
  jd: jdUT,
1580
2529
  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,
2530
+ ascendant: { longitude: Number(chart.houses.asc?.toFixed(4)), signe: signOf3(chart.houses.asc ?? 0) },
2531
+ milieuDuCiel: { longitude: Number(chart.houses.mc?.toFixed(4)), signe: signOf3(chart.houses.mc ?? 0) },
2532
+ // systemeMaisons = ce qui a RÉELLEMENT été calculé (chart.houses.system).
2533
+ // Diffère du demandé quand un système swiss-only (koch/regiomontanus/
2534
+ // campanus) retombe sur Placidus en mode AstraCore — honnêteté explicite.
2535
+ systemeMaisons: chart.houses.system,
2536
+ systemeMaisonsDemande: chart.houseSystem,
1590
2537
  zodiaque: chart.zodiac,
2538
+ ayanamsa: chart.ayanamsa,
1591
2539
  phaseLunaire: { phase: chart.moonPhase.phase, illumination: chart.moonPhase.illumination },
1592
2540
  retrogrades: chart.retrogrades,
1593
2541
  corps: formatPlanets(chart.planets),
1594
- aspects: chart.aspects
2542
+ aspects: chart.aspects,
2543
+ lotsHermetiques: chart.lots
1595
2544
  });
1596
2545
  }
1597
2546
  );
1598
2547
  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.",
2548
+ "get_transits",
2549
+ "Transits 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, tri\xE9s par orbe. Le c\u0153ur des \xAB pr\xE9visions \xBB.",
1601
2550
  {
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).")
2551
+ ...BIRTH_SHAPE,
2552
+ datetime: z.string().optional().describe('Instant du transit en ISO UTC (d\xE9faut : maintenant), ex. "2026-07-19T09:00:00Z".')
1604
2553
  },
1605
- async ({ latitude, longitude }) => {
1606
- const chart = computeCurrentSky2(latitude, longitude);
2554
+ async ({ date, time, timezone, latitude, longitude, datetime }) => {
2555
+ const { jdUT: natalJd } = localToUTC(date, time, timezone);
2556
+ const natal = computeChartFromJD2(natalJd, latitude, longitude).planets;
2557
+ const transitDate = datetime ? parseUtc(datetime) : /* @__PURE__ */ new Date();
2558
+ const transit = allPositions2(dateToJd(transitDate));
2559
+ const aspects = crossAspects(transit, natal);
1607
2560
  return jsonResult({
1608
- instant: (/* @__PURE__ */ new Date()).toISOString(),
1609
- jd: chart.JD,
2561
+ naissance: { date, time, timezone },
2562
+ instantTransit: transitDate.toISOString(),
1610
2563
  moteur: getActiveEngine(),
1611
- retrogrades: chart.retrogrades,
1612
- phaseLunaire: { phase: chart.moonPhase.phase, illumination: chart.moonPhase.illumination },
2564
+ nbAspects: aspects.length,
2565
+ aspects: aspects.map((a) => ({
2566
+ planeteTransit: a.a,
2567
+ aspect: a.typeFr,
2568
+ symbole: a.symbol,
2569
+ planeteNatale: a.b,
2570
+ orbe: a.orb,
2571
+ exact: a.exact
2572
+ }))
2573
+ });
2574
+ }
2575
+ );
2576
+ server.tool(
2577
+ "get_synastry",
2578
+ "Synastrie : aspects inter-th\xE8mes entre deux personnes (plan\xE8tes A \xD7 plan\xE8tes B), tri\xE9s par orbe, avec un r\xE9sum\xE9 harmonie/tension. R\xE9utilise la table d'aspects canonique du moteur.",
2579
+ {
2580
+ personA: z.object(BIRTH_SHAPE).describe("Naissance de la personne A."),
2581
+ personB: z.object(BIRTH_SHAPE).describe("Naissance de la personne B.")
2582
+ },
2583
+ async ({ personA, personB }) => {
2584
+ const a = computeChartFromJD2(localToUTC(personA.date, personA.time, personA.timezone).jdUT, personA.latitude, personA.longitude).planets;
2585
+ const b = computeChartFromJD2(localToUTC(personB.date, personB.time, personB.timezone).jdUT, personB.latitude, personB.longitude).planets;
2586
+ const aspects = crossAspects(a, b);
2587
+ return jsonResult({
2588
+ moteur: getActiveEngine(),
2589
+ resume: toneSummary(aspects),
2590
+ aspects: aspects.map((x) => ({
2591
+ planeteA: x.a,
2592
+ aspect: x.typeFr,
2593
+ symbole: x.symbol,
2594
+ tonalite: x.tone,
2595
+ planeteB: x.b,
2596
+ orbe: x.orb,
2597
+ exact: x.exact
2598
+ }))
2599
+ });
2600
+ }
2601
+ );
2602
+ server.tool(
2603
+ "get_composite_chart",
2604
+ "Th\xE8me composite par mi-points : pour chaque corps, le point milieu circulaire entre les deux th\xE8mes (convention Robert Hand). Repr\xE9sente la relation elle-m\xEAme.",
2605
+ {
2606
+ personA: z.object(BIRTH_SHAPE).describe("Naissance de la personne A."),
2607
+ personB: z.object(BIRTH_SHAPE).describe("Naissance de la personne B.")
2608
+ },
2609
+ async ({ personA, personB }) => {
2610
+ const a = computeChartFromJD2(localToUTC(personA.date, personA.time, personA.timezone).jdUT, personA.latitude, personA.longitude).planets;
2611
+ const b = computeChartFromJD2(localToUTC(personB.date, personB.time, personB.timezone).jdUT, personB.latitude, personB.longitude).planets;
2612
+ return jsonResult({
2613
+ moteur: getActiveEngine(),
2614
+ corpsComposite: compositePositions(a, b).map((c) => ({
2615
+ corps: c.corps,
2616
+ longitude: c.longitude,
2617
+ signe: SIGNS_FR[c.signIdx] ?? null
2618
+ }))
2619
+ });
2620
+ }
2621
+ );
2622
+ server.tool(
2623
+ "get_secondary_progressions",
2624
+ "Progressions secondaires (m\xE9thode \xAB un jour apr\xE8s la naissance = une ann\xE9e de vie \xBB) : positions plan\xE9taires progress\xE9es \xE0 une date cible. La Lune progress\xE9e (~1 signe / 2,5 ans) est la plus lue.",
2625
+ {
2626
+ ...BIRTH_SHAPE,
2627
+ targetDate: z.string().describe("Date pour laquelle progresser le th\xE8me, format YYYY-MM-DD.")
2628
+ },
2629
+ async ({ date, time, timezone, latitude, longitude, targetDate }) => {
2630
+ const { jdUT: natalJd } = localToUTC(date, time, timezone);
2631
+ const targetJd = dateToJd(parseUtc(targetDate + "T00:00:00Z"));
2632
+ const ageYears = (targetJd - natalJd) / JULIAN_YEAR;
2633
+ const progressedJd = natalJd + ageYears;
2634
+ const prog = allPositions2(progressedJd);
2635
+ return jsonResult({
2636
+ naissance: { date, time, timezone, latitude, longitude },
2637
+ dateCible: targetDate,
2638
+ ageAnnees: Number(ageYears.toFixed(2)),
2639
+ jdProgresse: progressedJd,
2640
+ moteur: getActiveEngine(),
2641
+ note: "Positions progress\xE9es (jour = an). Les maisons progress\xE9es ne sont pas incluses (m\xE9thode variable selon l'\xE9cole).",
2642
+ corpsProgresses: formatPlanets(prog)
2643
+ });
2644
+ }
2645
+ );
2646
+ server.tool(
2647
+ "get_solar_return",
2648
+ "Retour solaire d'une ann\xE9e donn\xE9e : l'instant exact o\xF9 le Soleil retrouve sa longitude natale, et le th\xE8me dress\xE9 \xE0 cet instant (pour le lieu de r\xE9sidence si fourni, sinon lieu de naissance).",
2649
+ {
2650
+ ...BIRTH_SHAPE,
2651
+ year: z.number().int().describe("Ann\xE9e du retour solaire, ex. 2026."),
2652
+ returnLatitude: z.number().min(-90).max(90).optional().describe("Latitude du lieu au moment du retour (d\xE9faut : lieu de naissance)."),
2653
+ returnLongitude: z.number().min(-180).max(180).optional().describe("Longitude du lieu au moment du retour (d\xE9faut : lieu de naissance).")
2654
+ },
2655
+ async ({ date, time, timezone, latitude, longitude, year, returnLatitude, returnLongitude }) => {
2656
+ const { jdUT: natalJd } = localToUTC(date, time, timezone);
2657
+ const natalSun = bodyLongitude("sun", natalJd);
2658
+ if (natalSun == null) throw new Error("Soleil natal introuvable (moteur).");
2659
+ const natalYear = Number(date.slice(0, 4));
2660
+ const approx = natalJd + (year - natalYear) * JULIAN_YEAR;
2661
+ const returnJd = findLongitudeReturn("sun", natalSun, approx - 5, approx + 5, 0.5);
2662
+ if (returnJd == null) throw new Error(`Retour solaire ${year} introuvable dans la fen\xEAtre calcul\xE9e.`);
2663
+ const lat = returnLatitude ?? latitude;
2664
+ const lng = returnLongitude ?? longitude;
2665
+ const chart = computeChartFromJD2(returnJd, lat, lng);
2666
+ return jsonResult({
2667
+ naissance: { date, time, timezone },
2668
+ annee: year,
2669
+ lieuRetour: { latitude: lat, longitude: lng },
2670
+ instantRetourUTC: jdToDate(returnJd).toISOString(),
2671
+ jd: returnJd,
2672
+ moteur: getActiveEngine(),
2673
+ ascendant: { longitude: Number(chart.houses.asc?.toFixed(4)), signe: signOf3(chart.houses.asc ?? 0) },
2674
+ milieuDuCiel: { longitude: Number(chart.houses.mc?.toFixed(4)), signe: signOf3(chart.houses.mc ?? 0) },
1613
2675
  corps: formatPlanets(chart.planets),
1614
2676
  aspects: chart.aspects
1615
2677
  });
1616
2678
  }
1617
2679
  );
2680
+ server.tool(
2681
+ "get_lunar_return",
2682
+ "Retour lunaire : le prochain instant, apr\xE8s une date de r\xE9f\xE9rence, o\xF9 la Lune retrouve sa longitude natale (~tous les 27,3 jours), et le th\xE8me \xE0 cet instant.",
2683
+ {
2684
+ ...BIRTH_SHAPE,
2685
+ after: z.string().optional().describe("Chercher le retour apr\xE8s cet instant ISO UTC (d\xE9faut : maintenant).")
2686
+ },
2687
+ async ({ date, time, timezone, latitude, longitude, after }) => {
2688
+ const { jdUT: natalJd } = localToUTC(date, time, timezone);
2689
+ const natalMoon = bodyLongitude("moon", natalJd);
2690
+ if (natalMoon == null) throw new Error("Lune natale introuvable (moteur).");
2691
+ const fromJd = dateToJd(after ? parseUtc(after) : /* @__PURE__ */ new Date());
2692
+ const returnJd = findLongitudeReturn("moon", natalMoon, fromJd, fromJd + 30, 0.25);
2693
+ if (returnJd == null) throw new Error("Retour lunaire introuvable dans les 30 jours.");
2694
+ const chart = computeChartFromJD2(returnJd, latitude, longitude);
2695
+ return jsonResult({
2696
+ naissance: { date, time, timezone },
2697
+ apres: jdToDate(fromJd).toISOString(),
2698
+ instantRetourUTC: jdToDate(returnJd).toISOString(),
2699
+ jd: returnJd,
2700
+ moteur: getActiveEngine(),
2701
+ ascendant: { longitude: Number(chart.houses.asc?.toFixed(4)), signe: signOf3(chart.houses.asc ?? 0) },
2702
+ corps: formatPlanets(chart.planets)
2703
+ });
2704
+ }
2705
+ );
2706
+ server.tool(
2707
+ "get_ingresses",
2708
+ "Ingr\xE8s : dates auxquelles un corps change de signe sur une plage donn\xE9e (entr\xE9es/sorties de signe), robuste aux passages r\xE9trogrades.",
2709
+ {
2710
+ body: BODY_ENUM.describe("Corps \xE0 suivre."),
2711
+ start: z.string().describe('D\xE9but de la plage, ISO UTC, ex. "2026-01-01T00:00:00Z".'),
2712
+ end: z.string().describe("Fin de la plage, ISO UTC. Plage max conseill\xE9e : quelques ann\xE9es.")
2713
+ },
2714
+ async ({ body, start, end }) => {
2715
+ const j0 = dateToJd(parseUtc(start));
2716
+ const j1 = dateToJd(parseUtc(end));
2717
+ if (j1 <= j0) throw new Error("`end` doit \xEAtre post\xE9rieur \xE0 `start`.");
2718
+ const step = body === "moon" ? 0.25 : 1;
2719
+ const events = findIngresses(body, j0, j1, step);
2720
+ return jsonResult({
2721
+ corps: body,
2722
+ plage: { start: parseUtc(start).toISOString(), end: parseUtc(end).toISOString() },
2723
+ moteur: getActiveEngine(),
2724
+ nbIngres: events.length,
2725
+ ingres: events.map((e) => ({
2726
+ dateUTC: jdToDate(e.jd).toISOString(),
2727
+ deSigne: SIGNS_FR[e.deSigneIdx] ?? null,
2728
+ versSigne: SIGNS_FR[e.versSigneIdx] ?? null
2729
+ }))
2730
+ });
2731
+ }
2732
+ );
2733
+ server.tool(
2734
+ "get_retrograde_windows",
2735
+ "Stations de r\xE9trogradation : dates auxquelles un corps change de direction (station r\xE9trograde ou directe) sur une plage donn\xE9e. Le Soleil et la Lune ne r\xE9trogradent jamais.",
2736
+ {
2737
+ body: RETRO_BODY_ENUM.describe("Corps \xE0 suivre (Mercure\u2192Pluton)."),
2738
+ start: z.string().describe("D\xE9but de la plage, ISO UTC."),
2739
+ end: z.string().describe("Fin de la plage, ISO UTC.")
2740
+ },
2741
+ async ({ body, start, end }) => {
2742
+ const j0 = dateToJd(parseUtc(start));
2743
+ const j1 = dateToJd(parseUtc(end));
2744
+ if (j1 <= j0) throw new Error("`end` doit \xEAtre post\xE9rieur \xE0 `start`.");
2745
+ const stations = findRetrogradeStations(body, j0, j1, 0.5);
2746
+ return jsonResult({
2747
+ corps: body,
2748
+ plage: { start: parseUtc(start).toISOString(), end: parseUtc(end).toISOString() },
2749
+ moteur: getActiveEngine(),
2750
+ nbStations: stations.length,
2751
+ stations: stations.map((s) => ({
2752
+ dateUTC: jdToDate(s.jd).toISOString(),
2753
+ station: s.station === "retrograde" ? "r\xE9trograde" : "directe"
2754
+ }))
2755
+ });
2756
+ }
2757
+ );
2758
+ server.tool(
2759
+ "get_eclipse_details",
2760
+ "D\xE9tail d'une \xE9clipse (magnitude, obscuration, type, s\xE9rie de Saros) pour une date d'\xE9clipse connue. N\xE9cessite le moteur swisseph (retourne null en mode AstraCore).",
2761
+ {
2762
+ datetime: z.string().describe(`Instant de l'\xE9clipse en ISO UTC, ex. "2026-08-12T17:46:00Z".`),
2763
+ kind: z.enum(["solar", "lunar"]).describe("Type d'\xE9clipse \xE0 d\xE9tailler.")
2764
+ },
2765
+ async ({ datetime, kind }) => {
2766
+ const jd2 = dateToJd(parseUtc(datetime));
2767
+ const details = kind === "solar" ? computeSolarEclipseDetailsSwiss(jd2) : computeLunarEclipseDetailsSwiss(jd2);
2768
+ return jsonResult({
2769
+ datetimeUTC: parseUtc(datetime).toISOString(),
2770
+ jd: jd2,
2771
+ moteur: getActiveEngine(),
2772
+ type: kind,
2773
+ details,
2774
+ note: details == null ? "D\xE9tail indisponible : moteur AstraCore actif (swisseph requis) ou date sans \xE9clipse. Passe en ASTRO_ENGINE=swisseph." : void 0
2775
+ });
2776
+ }
2777
+ );
2778
+ server.tool(
2779
+ "get_next_eclipse",
2780
+ "Trouve la prochaine (ou pr\xE9c\xE9dente) \xE9clipse solaire ou lunaire \xE0 partir d'une date : instant du maximum, type, et fen\xEAtres de contact. N\xE9cessite le moteur swisseph (retourne null en AstraCore).",
2781
+ {
2782
+ from: z.string().describe('Chercher \xE0 partir de cet instant ISO UTC, ex. "2026-01-01T00:00:00Z".'),
2783
+ kind: z.enum(["solar", "lunar"]).describe("Type d'\xE9clipse \xE0 chercher."),
2784
+ backward: z.boolean().optional().describe("true = chercher la pr\xE9c\xE9dente au lieu de la prochaine (d\xE9faut false).")
2785
+ },
2786
+ async ({ from, kind, backward }) => {
2787
+ const jd2 = dateToJd(parseUtc(from));
2788
+ const back = backward ?? false;
2789
+ const ecl = kind === "solar" ? findNextSolarEclipseSwiss(jd2, back) : findNextLunarEclipseSwiss(jd2, back);
2790
+ return jsonResult({
2791
+ depuis: parseUtc(from).toISOString(),
2792
+ sens: back ? "pr\xE9c\xE9dente" : "prochaine",
2793
+ type: kind,
2794
+ moteur: getActiveEngine(),
2795
+ eclipse: ecl == null ? null : {
2796
+ ...ecl,
2797
+ maximumUTC: jdToDate(ecl.jdMaximum).toISOString()
2798
+ },
2799
+ note: ecl == null ? "Recherche indisponible : moteur AstraCore actif (swisseph requis). Passe en ASTRO_ENGINE=swisseph." : void 0
2800
+ });
2801
+ }
2802
+ );
1618
2803
  server.tool(
1619
2804
  "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.",
2805
+ "Astrocartographie \xE0 un instant donn\xE9 : pour chaque corps, les m\xE9ridiens MC/IC et le nombre de points des courbes lever/coucher, plus les parans (croisements de lignes).",
1621
2806
  {
1622
- datetime: z.string().describe('Instant en ISO 8601 UTC, ex. "2026-07-18T14:00:00Z". Sans fuseau, interpr\xE9t\xE9 en UTC.')
2807
+ datetime: z.string().describe('Instant en ISO 8601 UTC, ex. "2026-07-18T14:00:00Z".')
1623
2808
  },
1624
2809
  async ({ datetime }) => {
1625
- const jd2 = jdFromDate(parseUtc(datetime));
2810
+ const jd2 = dateToJd(parseUtc(datetime));
1626
2811
  const acg = computeAstrocartography(jd2);
1627
2812
  return jsonResult({
1628
2813
  datetimeUTC: parseUtc(datetime).toISOString(),
1629
2814
  jd: jd2,
1630
2815
  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
2816
  lignes: acg.lines.map((l) => ({
1634
2817
  corps: l.key,
1635
2818
  meridienMC: Number(l.mcLng?.toFixed(4)),
@@ -1648,58 +2831,22 @@ server.tool(
1648
2831
  });
1649
2832
  }
1650
2833
  );
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
2834
  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.",
2835
+ "get_life_path",
2836
+ "Nombre du chemin de vie (num\xE9rologie pythagoricienne) calcul\xE9 depuis la date de naissance locale. Fait d\xE9terministe, jamais estim\xE9 par le mod\xE8le.",
1682
2837
  {
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".')
2838
+ date: z.string().describe("Date de naissance locale, format YYYY-MM-DD.")
1689
2839
  },
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
- });
2840
+ async ({ date }) => {
2841
+ return jsonResult({ date, cheminDeVie: computeLifePath(date) });
2842
+ }
2843
+ );
2844
+ server.tool(
2845
+ "get_engine_diagnostic",
2846
+ "Diagnostic du moteur de calcul : quel moteur est actif (swisseph sub-arcseconde vs AstraCore), si l'addon natif a charg\xE9, et pourquoi. Utile pour v\xE9rifier la pr\xE9cision dont vous b\xE9n\xE9ficiez.",
2847
+ {},
2848
+ async () => {
2849
+ return jsonResult({ moteur: getActiveEngine(), diagnostic: getEngineDiagnostic() });
1703
2850
  }
1704
2851
  );
1705
2852
  var HORIZONS_API = "https://ssd.jpl.nasa.gov/api/horizons.api";
@@ -1758,10 +2905,10 @@ server.tool(
1758
2905
  "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
2906
  {
1760
2907
  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).")
2908
+ bodies: z.array(BODY_ENUM).optional().describe("Sous-ensemble de corps \xE0 valider (d\xE9faut : les 10 classiques).")
1762
2909
  },
1763
2910
  async ({ datetime, bodies }) => {
1764
- const jd2 = jdFromDate(parseUtc(datetime));
2911
+ const jd2 = dateToJd(parseUtc(datetime));
1765
2912
  const ours = allPositions2(jd2);
1766
2913
  const keys = bodies ?? Object.keys(HORIZONS_CMD);
1767
2914
  const rows = await Promise.all(