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,89 @@
1
+ // Log levels
2
+ export const LogLevel = {
3
+ DEBUG: 'DEBUG',
4
+ INFO: 'INFO',
5
+ WARN: 'WARN',
6
+ ERROR: 'ERROR',
7
+ } as const;
8
+
9
+ export type LogLevelType = (typeof LogLevel)[keyof typeof LogLevel];
10
+
11
+ // Error categories
12
+ export const ErrorCategory = {
13
+ EPHEMERIS: 'EPHEMERIS',
14
+ CALCULATION: 'CALCULATION',
15
+ STORAGE: 'STORAGE',
16
+ VALIDATION: 'VALIDATION',
17
+ CHART_RENDERING: 'CHART_RENDERING',
18
+ SERVER: 'SERVER',
19
+ } as const;
20
+
21
+ export type ErrorCategoryType = (typeof ErrorCategory)[keyof typeof ErrorCategory];
22
+
23
+ // Chart theme colors
24
+ export const LIGHT_THEME_COLORS = [
25
+ '#ffffff', // Aries - White (fire)
26
+ '#c1e6d1', // Taurus - Mint (earth)
27
+ '#ffffff', // Gemini - White (air)
28
+ '#c1e6d1', // Cancer - Mint (water)
29
+ '#ffffff', // Leo - White (fire)
30
+ '#c1e6d1', // Virgo - Mint (earth)
31
+ '#ffffff', // Libra - White (air)
32
+ '#c1e6d1', // Scorpio - Mint (water)
33
+ '#ffffff', // Sagittarius - White (fire)
34
+ '#c1e6d1', // Capricorn - Mint (earth)
35
+ '#ffffff', // Aquarius - White (air)
36
+ '#c1e6d1', // Pisces - Mint (water)
37
+ ];
38
+
39
+ export const DARK_THEME_COLORS = [
40
+ '#282c34', // Aries - Dark (fire)
41
+ '#8545b0', // Taurus - Purple (earth)
42
+ '#282c34', // Gemini - Dark (air)
43
+ '#8545b0', // Cancer - Purple (water)
44
+ '#282c34', // Leo - Dark (fire)
45
+ '#8545b0', // Virgo - Purple (earth)
46
+ '#282c34', // Libra - Dark (air)
47
+ '#8545b0', // Scorpio - Purple (water)
48
+ '#282c34', // Sagittarius - Dark (fire)
49
+ '#8545b0', // Capricorn - Purple (earth)
50
+ '#282c34', // Aquarius - Dark (air)
51
+ '#8545b0', // Pisces - Purple (water)
52
+ ];
53
+
54
+ // Aspect colors for light theme
55
+ export const LIGHT_ASPECT_COLORS = {
56
+ conjunction: { degree: 0, orbit: 10, color: 'transparent' },
57
+ square: { degree: 90, orbit: 8, color: '#fb923c' }, // Orange
58
+ trine: { degree: 120, orbit: 8, color: '#34d399' }, // Emerald
59
+ opposition: { degree: 180, orbit: 10, color: '#a78bfa' }, // Purple
60
+ sextile: { degree: 60, orbit: 6, color: '#22d3ee' }, // Cyan
61
+ };
62
+
63
+ // Aspect colors for dark theme
64
+ export const DARK_ASPECT_COLORS = {
65
+ conjunction: { degree: 0, orbit: 10, color: 'transparent' },
66
+ square: { degree: 90, orbit: 8, color: '#f97316' }, // Orange
67
+ trine: { degree: 120, orbit: 8, color: '#10b981' }, // Emerald
68
+ opposition: { degree: 180, orbit: 10, color: '#8b5cf6' }, // Purple
69
+ sextile: { degree: 60, orbit: 6, color: '#06b6d4' }, // Cyan
70
+ };
71
+
72
+ /**
73
+ * Determine chart theme based on time of day
74
+ * Dark theme: 6 PM - 6 AM (18:00 - 06:00)
75
+ * Light theme: 6 AM - 6 PM (06:00 - 18:00)
76
+ *
77
+ * @param timezone - Optional IANA timezone to use (e.g., 'America/New_York'). Defaults to server local time.
78
+ * @returns 'dark' or 'light' theme
79
+ */
80
+ export function getDefaultTheme(timezone?: string): 'light' | 'dark' {
81
+ const now = new Date();
82
+ const hour = timezone
83
+ ? Number.parseInt(
84
+ now.toLocaleString('en-US', { timeZone: timezone, hour: '2-digit', hour12: false }),
85
+ 10
86
+ )
87
+ : now.getHours();
88
+ return hour >= 18 || hour < 6 ? 'dark' : 'light';
89
+ }
@@ -0,0 +1,226 @@
1
+ import { constants as Constants } from 'sweph';
2
+ import { ErrorCategory } from './constants.js';
3
+ import type { EphemerisCalculator } from './ephemeris.js';
4
+ import { logger } from './logger.js';
5
+ import type { EclipseInfo } from './types.js';
6
+
7
+ interface EclipseWhenResult {
8
+ flag: number;
9
+ error: string;
10
+ data: number[];
11
+ }
12
+
13
+ function isEclipseWhenResult(value: unknown): value is EclipseWhenResult {
14
+ if (typeof value !== 'object' || value == null) return false;
15
+ const obj = value as Record<string, unknown>;
16
+ return (
17
+ typeof obj.flag === 'number' &&
18
+ typeof obj.error === 'string' &&
19
+ Array.isArray(obj.data) &&
20
+ obj.data.every((v) => typeof v === 'number')
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Calculator for solar and lunar eclipses
26
+ *
27
+ * @remarks
28
+ * Finds upcoming solar and lunar eclipses using Swiss Ephemeris.
29
+ * Returns basic eclipse information including type and timing.
30
+ * TODO: Enhance with richer phase timing and visibility data.
31
+ */
32
+ export class EclipseCalculator {
33
+ /** Ephemeris calculator instance */
34
+ private ephem: EphemerisCalculator;
35
+
36
+ /**
37
+ * Create a new eclipse calculator
38
+ *
39
+ * @param ephem - Initialized ephemeris calculator
40
+ * @throws Error if ephemeris is not initialized
41
+ *
42
+ * @remarks
43
+ * The ephemeris calculator must be initialized before passing
44
+ * to the EclipseCalculator constructor.
45
+ */
46
+ constructor(ephem: EphemerisCalculator) {
47
+ this.ephem = ephem;
48
+ }
49
+
50
+ private callSolarEclipseWhenGlob(startJD: number): EclipseWhenResult {
51
+ if (!this.ephem.eph) {
52
+ throw new Error('Ephemeris not initialized');
53
+ }
54
+
55
+ // sweph typings currently declare `backwards` as number, but runtime expects boolean.
56
+ const callable = this.ephem.eph.sol_eclipse_when_glob as unknown as (
57
+ startJd: number,
58
+ flags: number,
59
+ eclipseType: number,
60
+ backwards: boolean
61
+ ) => unknown;
62
+
63
+ const raw = callable(startJD, Constants.SEFLG_SWIEPH, 0, false);
64
+ if (!isEclipseWhenResult(raw)) {
65
+ throw new Error('Unexpected sol_eclipse_when_glob result shape');
66
+ }
67
+
68
+ return raw;
69
+ }
70
+
71
+ /**
72
+ * Find the next solar eclipse after a given date
73
+ *
74
+ * @param startJD - Julian Day to start searching from
75
+ * @returns Solar eclipse info or null if none found
76
+ * @throws Error if ephemeris not initialized
77
+ *
78
+ * @remarks
79
+ * Searches globally for the next solar eclipse. Returns basic
80
+ * information about the eclipse type and maximum time.
81
+ */
82
+ findNextSolarEclipse(startJD: number): EclipseInfo | null {
83
+ if (!this.ephem.eph) {
84
+ throw new Error('Ephemeris not initialized');
85
+ }
86
+
87
+ try {
88
+ const result = this.callSolarEclipseWhenGlob(startJD);
89
+
90
+ if (result.error || !result.data || result.data.length < 1) {
91
+ return null;
92
+ }
93
+
94
+ const eclipseType = this.getSolarEclipseType(result.flag);
95
+
96
+ return {
97
+ type: 'solar',
98
+ date: this.ephem.julianDayToDate(result.data[0]),
99
+ eclipseType,
100
+ maxTime: this.ephem.julianDayToDate(result.data[0]),
101
+ };
102
+ } catch (e) {
103
+ logger.error(
104
+ 'Solar eclipse calculation failed',
105
+ ErrorCategory.CALCULATION,
106
+ e instanceof Error ? e : new Error(String(e))
107
+ );
108
+ return null;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Find the next lunar eclipse after a given date
114
+ *
115
+ * @param startJD - Julian Day to start searching from
116
+ * @returns Lunar eclipse info or null if none found
117
+ * @throws Error if ephemeris not initialized
118
+ *
119
+ * @remarks
120
+ * Searches globally for the next lunar eclipse. Returns basic
121
+ * information about the eclipse type and maximum time.
122
+ */
123
+ findNextLunarEclipse(startJD: number): EclipseInfo | null {
124
+ if (!this.ephem.eph) {
125
+ throw new Error('Ephemeris not initialized');
126
+ }
127
+
128
+ try {
129
+ const result = this.ephem.eph.lun_eclipse_when(startJD, Constants.SEFLG_SWIEPH, 0, false);
130
+
131
+ if (result.error || !result.data || result.data.length < 1) {
132
+ return null;
133
+ }
134
+
135
+ const eclipseType = this.getLunarEclipseType(result.flag);
136
+
137
+ return {
138
+ type: 'lunar',
139
+ date: this.ephem.julianDayToDate(result.data[0]),
140
+ eclipseType,
141
+ maxTime: this.ephem.julianDayToDate(result.data[0]),
142
+ };
143
+ } catch (e) {
144
+ logger.error(
145
+ 'Lunar eclipse calculation failed',
146
+ ErrorCategory.CALCULATION,
147
+ e instanceof Error ? e : new Error(String(e))
148
+ );
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Get the next eclipses (both solar and lunar) after a given date
155
+ *
156
+ * @param startJD - Julian Day to start searching from
157
+ * @returns Array of upcoming eclipses sorted by date
158
+ * @throws Error if ephemeris not initialized
159
+ *
160
+ * @remarks
161
+ * Finds the next solar and lunar eclipses. Returns them in
162
+ * chronological order. May return only one type if the other
163
+ * is too far in the future.
164
+ */
165
+ async getNextEclipses(startJD: number): Promise<EclipseInfo[] | null> {
166
+ if (!this.ephem.eph) {
167
+ throw new Error('Ephemeris not initialized');
168
+ }
169
+
170
+ try {
171
+ const solarEclipse = this.findNextSolarEclipse(startJD);
172
+ const lunarEclipse = this.findNextLunarEclipse(startJD);
173
+
174
+ const eclipses = await Promise.all([solarEclipse, lunarEclipse]);
175
+
176
+ const filteredEclipses = eclipses.filter((eclipse) => eclipse !== null);
177
+
178
+ if (filteredEclipses.length === 0) {
179
+ return null;
180
+ }
181
+
182
+ return filteredEclipses.sort((a, b) => a.date.getTime() - b.date.getTime());
183
+ } catch (e) {
184
+ logger.error(
185
+ 'Eclipse calculation failed',
186
+ ErrorCategory.CALCULATION,
187
+ e instanceof Error ? e : new Error(String(e))
188
+ );
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Get solar eclipse type from Swiss Ephemeris return code
195
+ *
196
+ * @param returnCode - Swiss Ephemeris solar eclipse return code
197
+ * @returns Human-readable eclipse type string
198
+ *
199
+ * @remarks
200
+ * Maps Swiss Ephemeris numeric codes to descriptive types.
201
+ * TODO: Should use constrained union types for better type safety.
202
+ */
203
+ private getSolarEclipseType(returnCode: number): string {
204
+ if (returnCode & Constants.SE_ECL_TOTAL) return 'Total';
205
+ if (returnCode & Constants.SE_ECL_ANNULAR) return 'Annular';
206
+ if (returnCode & Constants.SE_ECL_PARTIAL) return 'Partial';
207
+ return 'Unknown';
208
+ }
209
+
210
+ /**
211
+ * Get lunar eclipse type from Swiss Ephemeris return code
212
+ *
213
+ * @param returnCode - Swiss Ephemeris lunar eclipse return code
214
+ * @returns Human-readable eclipse type string
215
+ *
216
+ * @remarks
217
+ * Maps Swiss Ephemeris numeric codes to descriptive types.
218
+ * TODO: Should use constrained union types for better type safety.
219
+ */
220
+ private getLunarEclipseType(returnCode: number): string {
221
+ if (returnCode & Constants.SE_ECL_TOTAL) return 'Total';
222
+ if (returnCode & Constants.SE_ECL_PARTIAL) return 'Partial';
223
+ if (returnCode & Constants.SE_ECL_PENUMBRAL) return 'Penumbral';
224
+ return 'Unknown';
225
+ }
226
+ }