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,85 @@
1
+ import type { EphemerisCalculator } from './ephemeris.js';
2
+ import type { EclipseInfo } from './types.js';
3
+ /**
4
+ * Calculator for solar and lunar eclipses
5
+ *
6
+ * @remarks
7
+ * Finds upcoming solar and lunar eclipses using Swiss Ephemeris.
8
+ * Returns basic eclipse information including type and timing.
9
+ * TODO: Enhance with richer phase timing and visibility data.
10
+ */
11
+ export declare class EclipseCalculator {
12
+ /** Ephemeris calculator instance */
13
+ private ephem;
14
+ /**
15
+ * Create a new eclipse calculator
16
+ *
17
+ * @param ephem - Initialized ephemeris calculator
18
+ * @throws Error if ephemeris is not initialized
19
+ *
20
+ * @remarks
21
+ * The ephemeris calculator must be initialized before passing
22
+ * to the EclipseCalculator constructor.
23
+ */
24
+ constructor(ephem: EphemerisCalculator);
25
+ private callSolarEclipseWhenGlob;
26
+ /**
27
+ * Find the next solar eclipse after a given date
28
+ *
29
+ * @param startJD - Julian Day to start searching from
30
+ * @returns Solar eclipse info or null if none found
31
+ * @throws Error if ephemeris not initialized
32
+ *
33
+ * @remarks
34
+ * Searches globally for the next solar eclipse. Returns basic
35
+ * information about the eclipse type and maximum time.
36
+ */
37
+ findNextSolarEclipse(startJD: number): EclipseInfo | null;
38
+ /**
39
+ * Find the next lunar eclipse after a given date
40
+ *
41
+ * @param startJD - Julian Day to start searching from
42
+ * @returns Lunar eclipse info or null if none found
43
+ * @throws Error if ephemeris not initialized
44
+ *
45
+ * @remarks
46
+ * Searches globally for the next lunar eclipse. Returns basic
47
+ * information about the eclipse type and maximum time.
48
+ */
49
+ findNextLunarEclipse(startJD: number): EclipseInfo | null;
50
+ /**
51
+ * Get the next eclipses (both solar and lunar) after a given date
52
+ *
53
+ * @param startJD - Julian Day to start searching from
54
+ * @returns Array of upcoming eclipses sorted by date
55
+ * @throws Error if ephemeris not initialized
56
+ *
57
+ * @remarks
58
+ * Finds the next solar and lunar eclipses. Returns them in
59
+ * chronological order. May return only one type if the other
60
+ * is too far in the future.
61
+ */
62
+ getNextEclipses(startJD: number): Promise<EclipseInfo[] | null>;
63
+ /**
64
+ * Get solar eclipse type from Swiss Ephemeris return code
65
+ *
66
+ * @param returnCode - Swiss Ephemeris solar eclipse return code
67
+ * @returns Human-readable eclipse type string
68
+ *
69
+ * @remarks
70
+ * Maps Swiss Ephemeris numeric codes to descriptive types.
71
+ * TODO: Should use constrained union types for better type safety.
72
+ */
73
+ private getSolarEclipseType;
74
+ /**
75
+ * Get lunar eclipse type from Swiss Ephemeris return code
76
+ *
77
+ * @param returnCode - Swiss Ephemeris lunar eclipse return code
78
+ * @returns Human-readable eclipse type string
79
+ *
80
+ * @remarks
81
+ * Maps Swiss Ephemeris numeric codes to descriptive types.
82
+ * TODO: Should use constrained union types for better type safety.
83
+ */
84
+ private getLunarEclipseType;
85
+ }
@@ -0,0 +1,184 @@
1
+ import { constants as Constants } from 'sweph';
2
+ import { ErrorCategory } from './constants.js';
3
+ import { logger } from './logger.js';
4
+ function isEclipseWhenResult(value) {
5
+ if (typeof value !== 'object' || value == null)
6
+ return false;
7
+ const obj = value;
8
+ return (typeof obj.flag === 'number' &&
9
+ typeof obj.error === 'string' &&
10
+ Array.isArray(obj.data) &&
11
+ obj.data.every((v) => typeof v === 'number'));
12
+ }
13
+ /**
14
+ * Calculator for solar and lunar eclipses
15
+ *
16
+ * @remarks
17
+ * Finds upcoming solar and lunar eclipses using Swiss Ephemeris.
18
+ * Returns basic eclipse information including type and timing.
19
+ * TODO: Enhance with richer phase timing and visibility data.
20
+ */
21
+ export class EclipseCalculator {
22
+ /** Ephemeris calculator instance */
23
+ ephem;
24
+ /**
25
+ * Create a new eclipse calculator
26
+ *
27
+ * @param ephem - Initialized ephemeris calculator
28
+ * @throws Error if ephemeris is not initialized
29
+ *
30
+ * @remarks
31
+ * The ephemeris calculator must be initialized before passing
32
+ * to the EclipseCalculator constructor.
33
+ */
34
+ constructor(ephem) {
35
+ this.ephem = ephem;
36
+ }
37
+ callSolarEclipseWhenGlob(startJD) {
38
+ if (!this.ephem.eph) {
39
+ throw new Error('Ephemeris not initialized');
40
+ }
41
+ // sweph typings currently declare `backwards` as number, but runtime expects boolean.
42
+ const callable = this.ephem.eph.sol_eclipse_when_glob;
43
+ const raw = callable(startJD, Constants.SEFLG_SWIEPH, 0, false);
44
+ if (!isEclipseWhenResult(raw)) {
45
+ throw new Error('Unexpected sol_eclipse_when_glob result shape');
46
+ }
47
+ return raw;
48
+ }
49
+ /**
50
+ * Find the next solar eclipse after a given date
51
+ *
52
+ * @param startJD - Julian Day to start searching from
53
+ * @returns Solar eclipse info or null if none found
54
+ * @throws Error if ephemeris not initialized
55
+ *
56
+ * @remarks
57
+ * Searches globally for the next solar eclipse. Returns basic
58
+ * information about the eclipse type and maximum time.
59
+ */
60
+ findNextSolarEclipse(startJD) {
61
+ if (!this.ephem.eph) {
62
+ throw new Error('Ephemeris not initialized');
63
+ }
64
+ try {
65
+ const result = this.callSolarEclipseWhenGlob(startJD);
66
+ if (result.error || !result.data || result.data.length < 1) {
67
+ return null;
68
+ }
69
+ const eclipseType = this.getSolarEclipseType(result.flag);
70
+ return {
71
+ type: 'solar',
72
+ date: this.ephem.julianDayToDate(result.data[0]),
73
+ eclipseType,
74
+ maxTime: this.ephem.julianDayToDate(result.data[0]),
75
+ };
76
+ }
77
+ catch (e) {
78
+ logger.error('Solar eclipse calculation failed', ErrorCategory.CALCULATION, e instanceof Error ? e : new Error(String(e)));
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Find the next lunar eclipse after a given date
84
+ *
85
+ * @param startJD - Julian Day to start searching from
86
+ * @returns Lunar eclipse info or null if none found
87
+ * @throws Error if ephemeris not initialized
88
+ *
89
+ * @remarks
90
+ * Searches globally for the next lunar eclipse. Returns basic
91
+ * information about the eclipse type and maximum time.
92
+ */
93
+ findNextLunarEclipse(startJD) {
94
+ if (!this.ephem.eph) {
95
+ throw new Error('Ephemeris not initialized');
96
+ }
97
+ try {
98
+ const result = this.ephem.eph.lun_eclipse_when(startJD, Constants.SEFLG_SWIEPH, 0, false);
99
+ if (result.error || !result.data || result.data.length < 1) {
100
+ return null;
101
+ }
102
+ const eclipseType = this.getLunarEclipseType(result.flag);
103
+ return {
104
+ type: 'lunar',
105
+ date: this.ephem.julianDayToDate(result.data[0]),
106
+ eclipseType,
107
+ maxTime: this.ephem.julianDayToDate(result.data[0]),
108
+ };
109
+ }
110
+ catch (e) {
111
+ logger.error('Lunar eclipse calculation failed', ErrorCategory.CALCULATION, e instanceof Error ? e : new Error(String(e)));
112
+ return null;
113
+ }
114
+ }
115
+ /**
116
+ * Get the next eclipses (both solar and lunar) after a given date
117
+ *
118
+ * @param startJD - Julian Day to start searching from
119
+ * @returns Array of upcoming eclipses sorted by date
120
+ * @throws Error if ephemeris not initialized
121
+ *
122
+ * @remarks
123
+ * Finds the next solar and lunar eclipses. Returns them in
124
+ * chronological order. May return only one type if the other
125
+ * is too far in the future.
126
+ */
127
+ async getNextEclipses(startJD) {
128
+ if (!this.ephem.eph) {
129
+ throw new Error('Ephemeris not initialized');
130
+ }
131
+ try {
132
+ const solarEclipse = this.findNextSolarEclipse(startJD);
133
+ const lunarEclipse = this.findNextLunarEclipse(startJD);
134
+ const eclipses = await Promise.all([solarEclipse, lunarEclipse]);
135
+ const filteredEclipses = eclipses.filter((eclipse) => eclipse !== null);
136
+ if (filteredEclipses.length === 0) {
137
+ return null;
138
+ }
139
+ return filteredEclipses.sort((a, b) => a.date.getTime() - b.date.getTime());
140
+ }
141
+ catch (e) {
142
+ logger.error('Eclipse calculation failed', ErrorCategory.CALCULATION, e instanceof Error ? e : new Error(String(e)));
143
+ return null;
144
+ }
145
+ }
146
+ /**
147
+ * Get solar eclipse type from Swiss Ephemeris return code
148
+ *
149
+ * @param returnCode - Swiss Ephemeris solar eclipse return code
150
+ * @returns Human-readable eclipse type string
151
+ *
152
+ * @remarks
153
+ * Maps Swiss Ephemeris numeric codes to descriptive types.
154
+ * TODO: Should use constrained union types for better type safety.
155
+ */
156
+ getSolarEclipseType(returnCode) {
157
+ if (returnCode & Constants.SE_ECL_TOTAL)
158
+ return 'Total';
159
+ if (returnCode & Constants.SE_ECL_ANNULAR)
160
+ return 'Annular';
161
+ if (returnCode & Constants.SE_ECL_PARTIAL)
162
+ return 'Partial';
163
+ return 'Unknown';
164
+ }
165
+ /**
166
+ * Get lunar eclipse type from Swiss Ephemeris return code
167
+ *
168
+ * @param returnCode - Swiss Ephemeris lunar eclipse return code
169
+ * @returns Human-readable eclipse type string
170
+ *
171
+ * @remarks
172
+ * Maps Swiss Ephemeris numeric codes to descriptive types.
173
+ * TODO: Should use constrained union types for better type safety.
174
+ */
175
+ getLunarEclipseType(returnCode) {
176
+ if (returnCode & Constants.SE_ECL_TOTAL)
177
+ return 'Total';
178
+ if (returnCode & Constants.SE_ECL_PARTIAL)
179
+ return 'Partial';
180
+ if (returnCode & Constants.SE_ECL_PENUMBRAL)
181
+ return 'Penumbral';
182
+ return 'Unknown';
183
+ }
184
+ }
@@ -0,0 +1,120 @@
1
+ import sweph from 'sweph';
2
+ import { type PlanetPosition } from './types.js';
3
+ /**
4
+ * Ephemeris calculator wrapper for native Swiss Ephemeris (sweph)
5
+ *
6
+ * @remarks
7
+ * Provides a high-level interface for planetary calculations using the
8
+ * Swiss Ephemeris Node bindings. Handles initialization,
9
+ * coordinate conversions, and common astrological calculations.
10
+ *
11
+ * All longitudes are tropical (not sidereal) and geocentric.
12
+ */
13
+ export declare class EphemerisCalculator {
14
+ /** Native sweph module instance */
15
+ eph: typeof sweph | null;
16
+ /**
17
+ * Initialize the Swiss Ephemeris native module
18
+ *
19
+ * @returns Promise that resolves when initialization is complete
20
+ * @throws Error if module setup fails
21
+ *
22
+ * @remarks
23
+ * Must be called before any other methods. Loads the Swiss Ephemeris
24
+ * data files and prepares the calculation engine.
25
+ */
26
+ init(): Promise<void>;
27
+ /**
28
+ * Convert a JavaScript Date to Julian Day
29
+ *
30
+ * @param date - Date to convert (should be in UTC)
31
+ * @returns Julian Day number
32
+ * @throws Error if ephemeris not initialized
33
+ *
34
+ * @remarks
35
+ * Julian Day is a continuous count of days since noon Universal Time
36
+ * on January 1, 4713 BCE. It's the standard time system for astronomical
37
+ * calculations.
38
+ */
39
+ dateToJulianDay(date: Date): number;
40
+ /**
41
+ * Normalize angle to 0-360 degree range
42
+ *
43
+ * @param angle - Angle in degrees (may be negative or > 360)
44
+ * @returns Normalized angle in degrees (0-360)
45
+ *
46
+ * @remarks
47
+ * Uses modulo arithmetic to handle negative angles correctly.
48
+ * Example: -10° becomes 350°, 370° becomes 10°.
49
+ */
50
+ private normalizeAngle;
51
+ /**
52
+ * Get position of a single planet at a specific time
53
+ *
54
+ * @param planetId - Swiss Ephemeris planet ID (from PLANETS constant)
55
+ * @param jd - Julian Day for the calculation
56
+ * @returns Planet position with all relevant data
57
+ * @throws Error if ephemeris not initialized or invalid planet ID
58
+ *
59
+ * @remarks
60
+ * Returns tropical, geocentric coordinates. Includes zodiac sign
61
+ * calculation and retrograde status.
62
+ */
63
+ getPlanetPosition(planetId: number, jd: number): PlanetPosition;
64
+ /**
65
+ * Get positions for multiple planets at a specific time
66
+ *
67
+ * @param planetIds - Array of Swiss Ephemeris planet IDs
68
+ * @param jd - Julian Day for the calculation
69
+ * @returns Array of planet positions in the same order as planetIds
70
+ * @throws Error if ephemeris not initialized
71
+ *
72
+ * @remarks
73
+ * Convenience wrapper that maps over planetIds and calls getPlanetPosition for each.
74
+ */
75
+ getAllPlanets(jd: number, planetIds: number[]): PlanetPosition[];
76
+ /**
77
+ * Calculate angular distance between two planets
78
+ *
79
+ * @param lon1 - First planet's longitude
80
+ * @param lon2 - Second planet's longitude
81
+ * @returns Angular distance in degrees (0-180)
82
+ *
83
+ * @remarks
84
+ * Always returns the shorter arc between the two planets.
85
+ * For example, 350° and 10° have a distance of 20°, not 340°.
86
+ */
87
+ calculateAspectAngle(lon1: number, lon2: number): number;
88
+ /**
89
+ * Find all exact times when planet reaches a specific longitude
90
+ *
91
+ * @param planetId - Swiss Ephemeris planet ID
92
+ * @param targetLongitude - Target longitude in degrees (will be normalized to 0-360)
93
+ * @param startJD - Start of search window (Julian Day)
94
+ * @param endJD - End of search window (Julian Day)
95
+ * @param tolerance - Desired precision in degrees (default: 0.01°)
96
+ * @returns Array of Julian Days where crossings occur, sorted earliest-first, or empty array if none
97
+ * @throws Error if ephemeris not initialized or invalid inputs
98
+ *
99
+ * @remarks
100
+ * Uses multi-stage search: coarse scan for root detection, then bracket/minimum refinement.
101
+ * Endpoint-near-zero cases are collected directly as candidate roots.
102
+ * Only sign-change intervals are refined via bisection.
103
+ * Local minima of |diff| are refined to catch tangential no-sign-change roots.
104
+ * Returns all detected crossings in the interval, deduplicated within 1 minute,
105
+ * and sorted earliest-first. No guarantees are made outside the searched interval.
106
+ */
107
+ findExactTransitTimes(planetId: number, targetLongitude: number, startJD: number, endJD: number, tolerance?: number): number[];
108
+ /**
109
+ * Convert Julian Day to JavaScript Date
110
+ *
111
+ * @param jd - Julian Day number
112
+ * @returns JavaScript Date in UTC
113
+ * @throws Error if ephemeris not initialized
114
+ *
115
+ * @remarks
116
+ * The returned Date is always in UTC regardless of the original
117
+ * timezone of the calculation.
118
+ */
119
+ julianDayToDate(jd: number): Date;
120
+ }