ether-to-astro 1.0.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 (138) hide show
  1. package/.env.example +13 -0
  2. package/.github/pull_request_template.md +16 -0
  3. package/.github/workflows/release.yml +35 -0
  4. package/.github/workflows/test.yml +32 -0
  5. package/AGENTS.md +99 -0
  6. package/LICENSE +18 -0
  7. package/NOTICE.md +45 -0
  8. package/README.md +301 -0
  9. package/SETUP.md +70 -0
  10. package/TESTING_SUMMARY.md +238 -0
  11. package/TEST_SUITE_STATUS.md +218 -0
  12. package/biome.json +48 -0
  13. package/dist/astro-service.d.ts +98 -0
  14. package/dist/astro-service.js +496 -0
  15. package/dist/chart-types.d.ts +52 -0
  16. package/dist/chart-types.js +51 -0
  17. package/dist/charts.d.ts +125 -0
  18. package/dist/charts.js +324 -0
  19. package/dist/cli.d.ts +7 -0
  20. package/dist/cli.js +472 -0
  21. package/dist/constants.d.ts +81 -0
  22. package/dist/constants.js +76 -0
  23. package/dist/eclipses.d.ts +85 -0
  24. package/dist/eclipses.js +184 -0
  25. package/dist/ephemeris.d.ts +120 -0
  26. package/dist/ephemeris.js +379 -0
  27. package/dist/formatter.d.ts +2 -0
  28. package/dist/formatter.js +22 -0
  29. package/dist/houses.d.ts +82 -0
  30. package/dist/houses.js +169 -0
  31. package/dist/index.d.ts +14 -0
  32. package/dist/index.js +150 -0
  33. package/dist/loader.d.ts +2 -0
  34. package/dist/loader.js +31 -0
  35. package/dist/logger.d.ts +25 -0
  36. package/dist/logger.js +73 -0
  37. package/dist/profile-store.d.ts +48 -0
  38. package/dist/profile-store.js +156 -0
  39. package/dist/riseset.d.ts +82 -0
  40. package/dist/riseset.js +185 -0
  41. package/dist/storage.d.ts +10 -0
  42. package/dist/storage.js +40 -0
  43. package/dist/time-utils.d.ts +68 -0
  44. package/dist/time-utils.js +136 -0
  45. package/dist/tool-registry.d.ts +35 -0
  46. package/dist/tool-registry.js +307 -0
  47. package/dist/tool-result.d.ts +175 -0
  48. package/dist/tool-result.js +188 -0
  49. package/dist/transits.d.ts +108 -0
  50. package/dist/transits.js +263 -0
  51. package/dist/types.d.ts +450 -0
  52. package/dist/types.js +161 -0
  53. package/example-usage.md +131 -0
  54. package/natal-chart.json +187 -0
  55. package/package.json +61 -0
  56. package/scripts/download-ephemeris.js +115 -0
  57. package/setup.sh +21 -0
  58. package/src/astro-service.ts +710 -0
  59. package/src/chart-types.ts +125 -0
  60. package/src/charts.ts +399 -0
  61. package/src/cli.ts +694 -0
  62. package/src/constants.ts +89 -0
  63. package/src/eclipses.ts +226 -0
  64. package/src/ephemeris.ts +437 -0
  65. package/src/formatter.ts +25 -0
  66. package/src/houses.ts +202 -0
  67. package/src/index.ts +170 -0
  68. package/src/loader.ts +36 -0
  69. package/src/logger.ts +104 -0
  70. package/src/profile-store.ts +285 -0
  71. package/src/riseset.ts +229 -0
  72. package/src/time-utils.ts +167 -0
  73. package/src/tool-registry.ts +357 -0
  74. package/src/tool-result.ts +283 -0
  75. package/src/transits.ts +352 -0
  76. package/src/types.ts +547 -0
  77. package/tests/README.md +173 -0
  78. package/tests/TESTING_STRATEGY.md +178 -0
  79. package/tests/fixtures/bowen-yang-chart.ts +69 -0
  80. package/tests/fixtures/calculate-expected.ts +81 -0
  81. package/tests/fixtures/expected-results.ts +117 -0
  82. package/tests/fixtures/generate-expected-simple.ts +94 -0
  83. package/tests/helpers/date-fixtures.ts +15 -0
  84. package/tests/helpers/ephem.ts +11 -0
  85. package/tests/helpers/temp.ts +9 -0
  86. package/tests/setup.ts +11 -0
  87. package/tests/unit/astro-service.test.ts +323 -0
  88. package/tests/unit/chart-types.test.ts +18 -0
  89. package/tests/unit/charts-errors.test.ts +42 -0
  90. package/tests/unit/charts.test.ts +157 -0
  91. package/tests/unit/cli-commands.test.ts +82 -0
  92. package/tests/unit/cli-profiles.test.ts +128 -0
  93. package/tests/unit/cli.test.ts +191 -0
  94. package/tests/unit/constants.test.ts +26 -0
  95. package/tests/unit/correctness-critical.test.ts +408 -0
  96. package/tests/unit/eclipses.test.ts +108 -0
  97. package/tests/unit/ephemeris.test.ts +213 -0
  98. package/tests/unit/error-handling.test.ts +116 -0
  99. package/tests/unit/formatter.test.ts +29 -0
  100. package/tests/unit/houses-errors.test.ts +27 -0
  101. package/tests/unit/houses-validation.test.ts +164 -0
  102. package/tests/unit/houses.test.ts +205 -0
  103. package/tests/unit/profile-store.test.ts +163 -0
  104. package/tests/unit/real-user-charts.test.ts +148 -0
  105. package/tests/unit/riseset.test.ts +106 -0
  106. package/tests/unit/solver-edges.test.ts +197 -0
  107. package/tests/unit/time-utils-temporal.test.ts +303 -0
  108. package/tests/unit/time-utils.test.ts +173 -0
  109. package/tests/unit/tool-registry.test.ts +222 -0
  110. package/tests/unit/tool-result.test.ts +45 -0
  111. package/tests/unit/transit-correctness.test.ts +78 -0
  112. package/tests/unit/transits.test.ts +238 -0
  113. package/tests/validation/README.md +32 -0
  114. package/tests/validation/adapters/astrolog.ts +306 -0
  115. package/tests/validation/adapters/internal.ts +184 -0
  116. package/tests/validation/compare/eclipses.ts +47 -0
  117. package/tests/validation/compare/houses.ts +76 -0
  118. package/tests/validation/compare/positions.ts +104 -0
  119. package/tests/validation/compare/riseSet.ts +48 -0
  120. package/tests/validation/compare/roots.ts +90 -0
  121. package/tests/validation/compare/transits.ts +69 -0
  122. package/tests/validation/fixtures/astrolog-parity/core.ts +194 -0
  123. package/tests/validation/fixtures/eclipses/core.ts +14 -0
  124. package/tests/validation/fixtures/houses/core.ts +47 -0
  125. package/tests/validation/fixtures/positions/core.ts +159 -0
  126. package/tests/validation/fixtures/rise-set/core.ts +20 -0
  127. package/tests/validation/fixtures/roots/core.ts +47 -0
  128. package/tests/validation/fixtures/transits/core.ts +61 -0
  129. package/tests/validation/fixtures/transits/dst.ts +21 -0
  130. package/tests/validation/oracle.spec.ts +129 -0
  131. package/tests/validation/utils/denseRootOracle.ts +269 -0
  132. package/tests/validation/utils/fixtureTypes.ts +146 -0
  133. package/tests/validation/utils/report.ts +60 -0
  134. package/tests/validation/utils/tolerances.ts +23 -0
  135. package/tests/validation/validation.spec.ts +836 -0
  136. package/tools/color-picker.html +388 -0
  137. package/tsconfig.json +17 -0
  138. package/vitest.config.ts +31 -0
@@ -0,0 +1,108 @@
1
+ import type { EphemerisCalculator } from './ephemeris.js';
2
+ import { type NatalChart, type PlanetPosition, type Transit, type TransitResponse } from './types.js';
3
+ /**
4
+ * Deduplicate transits by keeping the best hit per aspect
5
+ * Priority: exact time > smallest orb > earliest exact time > deterministic tiebreakers
6
+ *
7
+ * @param transits - Array of transits to deduplicate
8
+ * @returns Deduplicated array with one transit per unique aspect key
9
+ *
10
+ * @remarks
11
+ * This is the production dedupe logic used by get_transits when collecting
12
+ * transits over multiple days. The key is: transitingPlanet + natalPlanet + aspect.
13
+ * Final tiebreakers use longitude and planet names for deterministic ordering.
14
+ */
15
+ export declare function deduplicateTransits(transits: Transit[]): Transit[];
16
+ /**
17
+ * Calculator for astrological transits and aspects
18
+ *
19
+ * @remarks
20
+ * Analyzes relationships between current planetary positions (transits)
21
+ * and natal chart positions. Calculates aspects, orbs, and exact timing
22
+ * when aspects become perfect.
23
+ */
24
+ export declare class TransitCalculator {
25
+ /** Ephemeris calculator instance */
26
+ private ephem;
27
+ private readonly exactTimeSupportedPlanetIds;
28
+ /**
29
+ * Create a new transit calculator
30
+ *
31
+ * @param ephem - Initialized ephemeris calculator
32
+ * @throws Error if ephemeris is not initialized
33
+ *
34
+ * @remarks
35
+ * The ephemeris calculator must be initialized before passing
36
+ * to the TransitCalculator constructor.
37
+ */
38
+ constructor(ephem: EphemerisCalculator);
39
+ /**
40
+ * Find all active transits between two sets of planets
41
+ *
42
+ * @param transitingPlanets - Current planetary positions
43
+ * @param natalPlanets - Birth chart planetary positions
44
+ * @param currentJD - Current Julian Day
45
+ * @returns Array of active transits with aspect details
46
+ *
47
+ * @remarks
48
+ * Checks all combinations of transiting and natal planets against
49
+ * all defined aspects. Includes exact time resolution for close aspects.
50
+ *
51
+ * Exact-time policy:
52
+ * - Solver computes candidate roots in a bounded interval, capped to PREVIEW_HORIZON_DAYS.
53
+ * - Product layer exposes exactTime only when root is within PREVIEW_HORIZON_DAYS.
54
+ * - exactTimeStatus communicates why exactTime may be omitted.
55
+ * - exactTimeStatus is only set when exact-time resolution is attempted
56
+ * (i.e. orb <= EXACT_TIME_ORB_THRESHOLD). When orb is wider, exactTimeStatus is undefined.
57
+ */
58
+ findTransits(transitingPlanets: PlanetPosition[], natalPlanets: PlanetPosition[], currentJD: number): Transit[];
59
+ /**
60
+ * Calculate exact time when a transit aspect becomes perfect
61
+ *
62
+ * @param transitingPlanet - Current transiting planet position
63
+ * @param natalPlanet - Natal planet position
64
+ * @param aspect - Aspect configuration
65
+ * @param currentJD - Current Julian Day
66
+ * @param heuristicApplying - Applying/separating estimate used as fallback selector
67
+ * @returns Exact-time resolution result including status and selected root
68
+ *
69
+ * @remarks
70
+ * Solver and product concerns are intentionally separated:
71
+ * - Solver: find candidate roots in [currentJD - searchWindow, currentJD + searchWindow]
72
+ * - Product: expose exactTime only when selected root is within PREVIEW_HORIZON_DAYS
73
+ *
74
+ * Status semantics:
75
+ * - within_preview: root found and exactTime exposed
76
+ * - outside_preview: root found but exactTime hidden by product policy
77
+ * - not_found: no root found in solver interval
78
+ * - unsupported_body: exact-time solver not supported for the transiting body
79
+ */
80
+ private calculateExactTransitTime;
81
+ /**
82
+ * Determine if an aspect is applying or separating
83
+ *
84
+ * @param transitLon - Transiting planet longitude
85
+ * @param natalLon - Natal planet longitude
86
+ * @param transitSpeed - Transiting planet's daily motion
87
+ * @param aspectAngle - Target aspect angle
88
+ * @returns true if applying, false if separating
89
+ *
90
+ * @remarks
91
+ * Applying: Aspect getting stronger (closer to exact)
92
+ * Separating: Aspect getting weaker (moving away from exact)
93
+ */
94
+ private isApplying;
95
+ /**
96
+ * Get all transits for a specific date
97
+ *
98
+ * @param natalChart - Birth chart data
99
+ * @param date - Date for transit calculation (interpreted as-is, no timezone conversion)
100
+ * @returns TransitResponse with all active transits
101
+ * @throws Error if natal chart is invalid
102
+ *
103
+ * @remarks
104
+ * Internal UTC primitive: calculates transits for the provided date/time as-is.
105
+ * Caller is responsible for any user-facing timezone semantics.
106
+ */
107
+ getTransitsForDate(date: Date, natalChart: NatalChart): Promise<TransitResponse>;
108
+ }
@@ -0,0 +1,263 @@
1
+ import { ASPECTS, PLANETS, } from './types.js';
2
+ // Constants for transit calculations
3
+ const EXACT_TIME_ORB_THRESHOLD = 2; // degrees - only calculate exact times within this orb (inclusive)
4
+ const EXACT_TIME_SEARCH_WINDOW = 5; // days - minimum solver search window for root finding
5
+ const PREVIEW_HORIZON_DAYS = 90; // days - product preview horizon for exposing exactTime
6
+ /**
7
+ * Deduplicate transits by keeping the best hit per aspect
8
+ * Priority: exact time > smallest orb > earliest exact time > deterministic tiebreakers
9
+ *
10
+ * @param transits - Array of transits to deduplicate
11
+ * @returns Deduplicated array with one transit per unique aspect key
12
+ *
13
+ * @remarks
14
+ * This is the production dedupe logic used by get_transits when collecting
15
+ * transits over multiple days. The key is: transitingPlanet + natalPlanet + aspect.
16
+ * Final tiebreakers use longitude and planet names for deterministic ordering.
17
+ */
18
+ export function deduplicateTransits(transits) {
19
+ const bestTransits = new Map();
20
+ for (const t of transits) {
21
+ const key = `${t.transitingPlanet}-${t.natalPlanet}-${t.aspect}`;
22
+ const existing = bestTransits.get(key);
23
+ if (!existing) {
24
+ bestTransits.set(key, t);
25
+ }
26
+ else {
27
+ // Priority: exact > smallest orb > earliest exact time > deterministic tiebreakers
28
+ const shouldReplace = (t.exactTime && !existing.exactTime) ||
29
+ (!!t.exactTime === !!existing.exactTime && t.orb < existing.orb) ||
30
+ (t.orb === existing.orb &&
31
+ t.exactTime &&
32
+ existing.exactTime &&
33
+ t.exactTime < existing.exactTime) ||
34
+ (t.orb === existing.orb &&
35
+ !t.exactTime &&
36
+ !existing.exactTime &&
37
+ t.transitLongitude < existing.transitLongitude) ||
38
+ (t.orb === existing.orb &&
39
+ !t.exactTime &&
40
+ !existing.exactTime &&
41
+ t.transitLongitude === existing.transitLongitude &&
42
+ t.natalLongitude < existing.natalLongitude) ||
43
+ (t.orb === existing.orb &&
44
+ !t.exactTime &&
45
+ !existing.exactTime &&
46
+ t.transitLongitude === existing.transitLongitude &&
47
+ t.natalLongitude === existing.natalLongitude &&
48
+ t.transitingPlanet < existing.transitingPlanet) ||
49
+ (t.orb === existing.orb &&
50
+ !t.exactTime &&
51
+ !existing.exactTime &&
52
+ t.transitLongitude === existing.transitLongitude &&
53
+ t.natalLongitude === existing.natalLongitude &&
54
+ t.transitingPlanet === existing.transitingPlanet &&
55
+ t.natalPlanet < existing.natalPlanet);
56
+ if (shouldReplace) {
57
+ bestTransits.set(key, t);
58
+ }
59
+ }
60
+ }
61
+ return Array.from(bestTransits.values());
62
+ }
63
+ /**
64
+ * Calculator for astrological transits and aspects
65
+ *
66
+ * @remarks
67
+ * Analyzes relationships between current planetary positions (transits)
68
+ * and natal chart positions. Calculates aspects, orbs, and exact timing
69
+ * when aspects become perfect.
70
+ */
71
+ export class TransitCalculator {
72
+ /** Ephemeris calculator instance */
73
+ ephem;
74
+ exactTimeSupportedPlanetIds = new Set(Object.values(PLANETS));
75
+ /**
76
+ * Create a new transit calculator
77
+ *
78
+ * @param ephem - Initialized ephemeris calculator
79
+ * @throws Error if ephemeris is not initialized
80
+ *
81
+ * @remarks
82
+ * The ephemeris calculator must be initialized before passing
83
+ * to the TransitCalculator constructor.
84
+ */
85
+ constructor(ephem) {
86
+ this.ephem = ephem;
87
+ }
88
+ /**
89
+ * Find all active transits between two sets of planets
90
+ *
91
+ * @param transitingPlanets - Current planetary positions
92
+ * @param natalPlanets - Birth chart planetary positions
93
+ * @param currentJD - Current Julian Day
94
+ * @returns Array of active transits with aspect details
95
+ *
96
+ * @remarks
97
+ * Checks all combinations of transiting and natal planets against
98
+ * all defined aspects. Includes exact time resolution for close aspects.
99
+ *
100
+ * Exact-time policy:
101
+ * - Solver computes candidate roots in a bounded interval, capped to PREVIEW_HORIZON_DAYS.
102
+ * - Product layer exposes exactTime only when root is within PREVIEW_HORIZON_DAYS.
103
+ * - exactTimeStatus communicates why exactTime may be omitted.
104
+ * - exactTimeStatus is only set when exact-time resolution is attempted
105
+ * (i.e. orb <= EXACT_TIME_ORB_THRESHOLD). When orb is wider, exactTimeStatus is undefined.
106
+ */
107
+ findTransits(transitingPlanets, natalPlanets, currentJD) {
108
+ const transits = [];
109
+ for (const transitPlanet of transitingPlanets) {
110
+ for (const natalPlanet of natalPlanets) {
111
+ const angle = this.ephem.calculateAspectAngle(transitPlanet.longitude, natalPlanet.longitude);
112
+ for (const aspect of ASPECTS) {
113
+ const orb = Math.abs(angle - aspect.angle);
114
+ if (orb <= aspect.orb) {
115
+ const heuristicApplying = this.isApplying(transitPlanet.longitude, natalPlanet.longitude, transitPlanet.speed, aspect.angle);
116
+ let exactTime;
117
+ let exactTimeStatus;
118
+ let isApplying = heuristicApplying;
119
+ if (orb <= EXACT_TIME_ORB_THRESHOLD) {
120
+ const exactResult = this.calculateExactTransitTime(transitPlanet, natalPlanet, aspect, currentJD, heuristicApplying);
121
+ exactTime = exactResult.exactTime;
122
+ exactTimeStatus = exactResult.status;
123
+ // Strong applying/separating policy:
124
+ // selected root in future => applying, past => separating
125
+ if (exactResult.selectedRoot != null) {
126
+ isApplying = exactResult.selectedRoot >= currentJD;
127
+ }
128
+ }
129
+ transits.push({
130
+ transitingPlanet: transitPlanet.planet,
131
+ natalPlanet: natalPlanet.planet,
132
+ aspect: aspect.name,
133
+ orb,
134
+ exactTime,
135
+ exactTimeStatus,
136
+ isApplying,
137
+ transitLongitude: transitPlanet.longitude,
138
+ natalLongitude: natalPlanet.longitude,
139
+ });
140
+ }
141
+ }
142
+ }
143
+ }
144
+ return transits.sort((a, b) => a.orb - b.orb);
145
+ }
146
+ /**
147
+ * Calculate exact time when a transit aspect becomes perfect
148
+ *
149
+ * @param transitingPlanet - Current transiting planet position
150
+ * @param natalPlanet - Natal planet position
151
+ * @param aspect - Aspect configuration
152
+ * @param currentJD - Current Julian Day
153
+ * @param heuristicApplying - Applying/separating estimate used as fallback selector
154
+ * @returns Exact-time resolution result including status and selected root
155
+ *
156
+ * @remarks
157
+ * Solver and product concerns are intentionally separated:
158
+ * - Solver: find candidate roots in [currentJD - searchWindow, currentJD + searchWindow]
159
+ * - Product: expose exactTime only when selected root is within PREVIEW_HORIZON_DAYS
160
+ *
161
+ * Status semantics:
162
+ * - within_preview: root found and exactTime exposed
163
+ * - outside_preview: root found but exactTime hidden by product policy
164
+ * - not_found: no root found in solver interval
165
+ * - unsupported_body: exact-time solver not supported for the transiting body
166
+ */
167
+ calculateExactTransitTime(transitingPlanet, natalPlanet, aspect, currentJD, heuristicApplying) {
168
+ // For non-conjunction/opposition aspects, there are 2 possible target longitudes
169
+ const target1 = (natalPlanet.longitude + aspect.angle) % 360;
170
+ const target2 = (natalPlanet.longitude - aspect.angle + 360) % 360;
171
+ const planetId = transitingPlanet.planetId;
172
+ // Skip exact time calculation for unsupported bodies
173
+ if (!this.exactTimeSupportedPlanetIds.has(planetId)) {
174
+ return { status: 'unsupported_body', selectedRoot: null };
175
+ }
176
+ // Calculate dynamic search window based on planet speed
177
+ // Slow movers (Saturn/Uranus/Neptune/Pluto) need wider windows,
178
+ // but solver horizon is capped to product preview horizon for bounded compute and aligned policy.
179
+ const speed = Math.abs(transitingPlanet.speed);
180
+ const daysToMove2Deg = speed > 0 ? 2 / speed : 30;
181
+ const searchWindow = Math.min(Math.max(daysToMove2Deg, EXACT_TIME_SEARCH_WINDOW), PREVIEW_HORIZON_DAYS);
182
+ // Search both targets (for conjunction/opposition, they're the same or opposite)
183
+ // Solver returns all roots sorted earliest-first
184
+ const roots1 = this.ephem.findExactTransitTimes(planetId, target1, currentJD - searchWindow, currentJD + searchWindow);
185
+ const roots2 = aspect.angle !== 0 && aspect.angle !== 180
186
+ ? this.ephem.findExactTransitTimes(planetId, target2, currentJD - searchWindow, currentJD + searchWindow)
187
+ : [];
188
+ // Combine all roots from both targets and sort
189
+ const allRoots = [...roots1, ...roots2].sort((a, b) => a - b);
190
+ // Split into past and future roots
191
+ const futureRoots = allRoots.filter((jd) => jd >= currentJD);
192
+ const pastRoots = allRoots.filter((jd) => jd < currentJD);
193
+ // Select root based on applying/separating:
194
+ // - Applying: pick earliest future root (approaching exact)
195
+ // - Separating: pick latest past root (just passed exact), fallback to earliest future
196
+ const selectedRoot = heuristicApplying
197
+ ? (futureRoots[0] ?? null)
198
+ : (pastRoots[pastRoots.length - 1] ?? futureRoots[0] ?? null);
199
+ if (selectedRoot === null) {
200
+ return { status: 'not_found', selectedRoot: null };
201
+ }
202
+ const daysUntilExact = selectedRoot - currentJD;
203
+ // Product policy: only show exact time if within preview horizon
204
+ // For outer planets, it's normal to be within 2° orb but months from exact
205
+ if (Math.abs(daysUntilExact) > PREVIEW_HORIZON_DAYS) {
206
+ return { status: 'outside_preview', selectedRoot };
207
+ }
208
+ return {
209
+ exactTime: this.ephem.julianDayToDate(selectedRoot),
210
+ status: 'within_preview',
211
+ selectedRoot,
212
+ };
213
+ }
214
+ /**
215
+ * Determine if an aspect is applying or separating
216
+ *
217
+ * @param transitLon - Transiting planet longitude
218
+ * @param natalLon - Natal planet longitude
219
+ * @param transitSpeed - Transiting planet's daily motion
220
+ * @param aspectAngle - Target aspect angle
221
+ * @returns true if applying, false if separating
222
+ *
223
+ * @remarks
224
+ * Applying: Aspect getting stronger (closer to exact)
225
+ * Separating: Aspect getting weaker (moving away from exact)
226
+ */
227
+ isApplying(transitLon, natalLon, transitSpeed, aspectAngle) {
228
+ if (transitSpeed === 0)
229
+ return false;
230
+ const currentAngle = this.ephem.calculateAspectAngle(transitLon, natalLon);
231
+ const futureAngle = this.ephem.calculateAspectAngle(transitLon + transitSpeed, natalLon);
232
+ return Math.abs(futureAngle - aspectAngle) < Math.abs(currentAngle - aspectAngle);
233
+ }
234
+ /**
235
+ * Get all transits for a specific date
236
+ *
237
+ * @param natalChart - Birth chart data
238
+ * @param date - Date for transit calculation (interpreted as-is, no timezone conversion)
239
+ * @returns TransitResponse with all active transits
240
+ * @throws Error if natal chart is invalid
241
+ *
242
+ * @remarks
243
+ * Internal UTC primitive: calculates transits for the provided date/time as-is.
244
+ * Caller is responsible for any user-facing timezone semantics.
245
+ */
246
+ async getTransitsForDate(date, natalChart) {
247
+ const jd = this.ephem.dateToJulianDay(date);
248
+ // Get all major planets (Sun through Pluto)
249
+ const planetIds = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
250
+ const transitingPlanets = this.ephem.getAllPlanets(jd, planetIds);
251
+ const transits = this.findTransits(transitingPlanets, natalChart.planets || [], jd);
252
+ // Convert Transit[] to TransitData[] (serialize Date to ISO string)
253
+ const transitData = transits.map((t) => ({
254
+ ...t,
255
+ exactTime: t.exactTime?.toISOString(),
256
+ }));
257
+ return {
258
+ date: date.toISOString().split('T')[0],
259
+ timezone: 'UTC',
260
+ transits: transitData,
261
+ };
262
+ }
263
+ }