ether-to-astro 1.2.0 → 1.3.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 (66) hide show
  1. package/README.md +15 -5
  2. package/dist/astro-service/chart-output-service.d.ts +44 -0
  3. package/dist/astro-service/chart-output-service.js +110 -0
  4. package/dist/astro-service/date-input.d.ts +14 -0
  5. package/dist/astro-service/date-input.js +30 -0
  6. package/dist/astro-service/electional-service.d.ts +45 -0
  7. package/dist/astro-service/electional-service.js +305 -0
  8. package/dist/astro-service/natal-service.d.ts +41 -0
  9. package/dist/astro-service/natal-service.js +179 -0
  10. package/dist/astro-service/rising-sign-service.d.ts +37 -0
  11. package/dist/astro-service/rising-sign-service.js +137 -0
  12. package/dist/astro-service/service-types.d.ts +82 -0
  13. package/dist/astro-service/service-types.js +1 -0
  14. package/dist/astro-service/shared.d.ts +65 -0
  15. package/dist/astro-service/shared.js +98 -0
  16. package/dist/astro-service/sky-service.d.ts +48 -0
  17. package/dist/astro-service/sky-service.js +144 -0
  18. package/dist/astro-service/transit-service.d.ts +82 -0
  19. package/dist/astro-service/transit-service.js +353 -0
  20. package/dist/astro-service.d.ts +101 -89
  21. package/dist/astro-service.js +162 -1042
  22. package/dist/tool-registry.js +1 -1
  23. package/docs/product/architecture-boundaries.md +8 -0
  24. package/docs/releases/1.3.0.md +51 -0
  25. package/docs/releases/README.md +17 -0
  26. package/package.json +4 -1
  27. package/src/astro-service/chart-output-service.ts +155 -0
  28. package/src/astro-service/date-input.ts +40 -0
  29. package/src/astro-service/electional-service.ts +395 -0
  30. package/src/astro-service/natal-service.ts +235 -0
  31. package/src/astro-service/rising-sign-service.ts +181 -0
  32. package/src/astro-service/service-types.ts +90 -0
  33. package/src/astro-service/shared.ts +128 -0
  34. package/src/astro-service/sky-service.ts +191 -0
  35. package/src/astro-service/transit-service.ts +507 -0
  36. package/src/astro-service.ts +177 -1386
  37. package/src/tool-registry.ts +1 -1
  38. package/tests/README.md +15 -0
  39. package/tests/property/electional-service.property.test.ts +67 -0
  40. package/tests/property/helpers/arbitraries.ts +126 -0
  41. package/tests/property/helpers/config.ts +52 -0
  42. package/tests/property/helpers/runtime.ts +12 -0
  43. package/tests/property/houses.property.test.ts +74 -0
  44. package/tests/property/rising-sign-service.property.test.ts +255 -0
  45. package/tests/property/service-transits.property.test.ts +154 -0
  46. package/tests/property/time-utils.property.test.ts +91 -0
  47. package/tests/property/transits.property.test.ts +113 -0
  48. package/tests/unit/astro-service/chart-output-service.test.ts +102 -0
  49. package/tests/unit/astro-service/electional-service.test.ts +182 -0
  50. package/tests/unit/astro-service/natal-service.test.ts +126 -0
  51. package/tests/unit/astro-service/rising-sign-service.test.ts +145 -0
  52. package/tests/unit/astro-service/sky-service.test.ts +130 -0
  53. package/tests/unit/astro-service/transit-service.test.ts +312 -0
  54. package/tests/unit/astro-service.test.ts +136 -781
  55. package/tests/unit/rising-sign-windows.test.ts +93 -0
  56. package/tests/unit/tool-registry.test.ts +11 -0
  57. package/tests/validation/README.md +14 -0
  58. package/tests/validation/adapters/internal.ts +234 -4
  59. package/tests/validation/compare/electional.ts +151 -0
  60. package/tests/validation/compare/rising-sign-windows.ts +347 -0
  61. package/tests/validation/compare/service-transits.ts +205 -0
  62. package/tests/validation/fixtures/electional/core.ts +88 -0
  63. package/tests/validation/fixtures/rising-sign-windows/core.ts +57 -0
  64. package/tests/validation/fixtures/service-transits/core.ts +89 -0
  65. package/tests/validation/utils/fixtureTypes.ts +139 -1
  66. package/tests/validation/validation.spec.ts +82 -0
@@ -0,0 +1,179 @@
1
+ import { localToUTC, utcToLocal } from '../time-utils.js';
2
+ import { PLANETS, ZODIAC_SIGNS } from '../types.js';
3
+ import { resolveHouseSystem } from './shared.js';
4
+ /**
5
+ * Internal natal/chart-state workflow used by `AstroService`.
6
+ *
7
+ * @remarks
8
+ * This module owns natal chart initialization, house resolution, and basic
9
+ * server-status serialization while the public `AstroService` facade preserves
10
+ * the existing contract for MCP and CLI callers.
11
+ */
12
+ export class NatalService {
13
+ ephem;
14
+ houseCalc;
15
+ mcpStartupDefaults;
16
+ isInitialized;
17
+ constructor(deps) {
18
+ this.ephem = deps.ephem;
19
+ this.houseCalc = deps.houseCalc;
20
+ this.mcpStartupDefaults = deps.mcpStartupDefaults;
21
+ this.isInitialized = deps.isInitialized;
22
+ }
23
+ /**
24
+ * Build and cache the shared natal chart payload used by later workflows.
25
+ */
26
+ setNatalChart(input) {
27
+ const requestedHouseSystem = input.house_system ?? null;
28
+ const chart = {
29
+ name: input.name,
30
+ birthDate: {
31
+ year: input.year,
32
+ month: input.month,
33
+ day: input.day,
34
+ hour: input.hour,
35
+ minute: input.minute,
36
+ },
37
+ location: {
38
+ latitude: input.latitude,
39
+ longitude: input.longitude,
40
+ timezone: input.timezone,
41
+ },
42
+ };
43
+ const birthTimeDisambiguation = input.birth_time_disambiguation ?? 'reject';
44
+ const utcDate = localToUTC(chart.birthDate, chart.location.timezone, birthTimeDisambiguation);
45
+ const utcComponents = utcToLocal(utcDate, 'UTC');
46
+ const jd = this.ephem.dateToJulianDay(utcDate);
47
+ const planetIds = Object.values(PLANETS);
48
+ const positions = this.ephem.getAllPlanets(jd, planetIds);
49
+ const isPolar = Math.abs(chart.location.latitude) > 66;
50
+ let houseSystem = requestedHouseSystem || 'P';
51
+ if (isPolar && houseSystem === 'P') {
52
+ houseSystem = 'W';
53
+ }
54
+ const houses = this.houseCalc.calculateHouses(jd, chart.location.latitude, chart.location.longitude, houseSystem);
55
+ const storedChart = {
56
+ ...chart,
57
+ planets: positions,
58
+ julianDay: jd,
59
+ houseSystem: houses.system,
60
+ requestedHouseSystem: requestedHouseSystem ?? undefined,
61
+ utcDateTime: utcComponents,
62
+ };
63
+ const sun = positions.find((position) => position.planet === 'Sun');
64
+ const moon = positions.find((position) => position.planet === 'Moon');
65
+ if (!sun || !moon) {
66
+ throw new Error('Ephemeris failed to compute Sun/Moon positions for natal chart.');
67
+ }
68
+ const formatDegree = (longitude) => {
69
+ const sign = ZODIAC_SIGNS[Math.floor(longitude / 30)];
70
+ const degree = longitude % 30;
71
+ return `${degree.toFixed(0)}° ${sign}`;
72
+ };
73
+ const localTimeStr = `${chart.birthDate.month}/${chart.birthDate.day}/${chart.birthDate.year} ${chart.birthDate.hour}:${String(chart.birthDate.minute).padStart(2, '0')}`;
74
+ const utcTimeStr = `${utcComponents.month}/${utcComponents.day}/${utcComponents.year} ${utcComponents.hour}:${String(utcComponents.minute).padStart(2, '0')} UTC`;
75
+ const systemNames = {
76
+ P: 'Placidus',
77
+ W: 'Whole Sign',
78
+ K: 'Koch',
79
+ E: 'Equal',
80
+ };
81
+ const latDir = chart.location.latitude >= 0 ? 'N' : 'S';
82
+ const lonDir = chart.location.longitude >= 0 ? 'E' : 'W';
83
+ const latAbs = Math.abs(chart.location.latitude);
84
+ const lonAbs = Math.abs(chart.location.longitude);
85
+ const feedback = [
86
+ `Natal chart saved for ${chart.name}`,
87
+ '',
88
+ 'Birth Details:',
89
+ `- Local Time: ${localTimeStr} (${chart.location.timezone})`,
90
+ `- UTC Time: ${utcTimeStr}`,
91
+ `- Location: ${latAbs.toFixed(2)}°${latDir}, ${lonAbs.toFixed(2)}°${lonDir}`,
92
+ '',
93
+ 'Chart Angles:',
94
+ `- Sun: ${formatDegree(sun.longitude)}`,
95
+ `- Moon: ${formatDegree(moon.longitude)}`,
96
+ `- Ascendant: ${formatDegree(houses.ascendant)}`,
97
+ `- MC: ${formatDegree(houses.mc)}`,
98
+ '',
99
+ `House System: ${systemNames[houses.system] || houses.system}`,
100
+ ];
101
+ if (isPolar && houses.system !== houseSystem) {
102
+ feedback.push('', `Note: Polar latitude detected (${chart.location.latitude.toFixed(1)}°). Requested ${systemNames[houseSystem]}, using ${systemNames[houses.system]} instead.`);
103
+ }
104
+ else if (isPolar) {
105
+ feedback.push('', `Note: Polar latitude detected (${chart.location.latitude.toFixed(1)}°). Using ${systemNames[houses.system]} house system.`);
106
+ }
107
+ const structuredData = {
108
+ name: chart.name,
109
+ birthTime: {
110
+ local: localTimeStr,
111
+ utc: utcTimeStr,
112
+ timezone: chart.location.timezone,
113
+ },
114
+ location: {
115
+ latitude: chart.location.latitude,
116
+ longitude: chart.location.longitude,
117
+ },
118
+ julianDay: jd,
119
+ requestedHouseSystem,
120
+ resolvedHouseSystem: houses.system,
121
+ angles: {
122
+ sun: formatDegree(sun.longitude),
123
+ moon: formatDegree(moon.longitude),
124
+ ascendant: formatDegree(houses.ascendant),
125
+ mc: formatDegree(houses.mc),
126
+ },
127
+ isPolar,
128
+ };
129
+ return {
130
+ chart: storedChart,
131
+ data: structuredData,
132
+ text: feedback.join('\n'),
133
+ };
134
+ }
135
+ /**
136
+ * Calculate house cusps and angles for a natal chart.
137
+ */
138
+ getHouses(natalChart, input = {}) {
139
+ const system = resolveHouseSystem(natalChart, this.mcpStartupDefaults, input.system);
140
+ if (!natalChart.julianDay) {
141
+ throw new Error('Natal chart is missing julianDay. Re-run set_natal_chart to fix.');
142
+ }
143
+ const houses = this.houseCalc.calculateHouses(natalChart.julianDay, natalChart.location.latitude, natalChart.location.longitude, system);
144
+ const humanLines = houses.cusps
145
+ .slice(1)
146
+ .map((degree, index) => {
147
+ const sign = ZODIAC_SIGNS[Math.floor(degree / 30)];
148
+ return `House ${index + 1}: ${(degree % 30).toFixed(2)}° ${sign}`;
149
+ })
150
+ .join('\n');
151
+ const humanText = `Houses (${houses.system}):\nAsc: ${houses.ascendant.toFixed(2)}° | MC: ${houses.mc.toFixed(2)}°\n\n${humanLines}`;
152
+ return {
153
+ data: houses,
154
+ text: humanText,
155
+ };
156
+ }
157
+ /**
158
+ * Summarize process-local server state and configured startup defaults.
159
+ */
160
+ getServerStatus(natalChart) {
161
+ const statusData = {
162
+ serverVersion: '1.0.0',
163
+ hasNatalChart: natalChart !== null,
164
+ natalChartName: natalChart?.name ?? null,
165
+ natalChartTimezone: natalChart?.location.timezone ?? null,
166
+ startupDefaults: {
167
+ preferredTimezone: this.mcpStartupDefaults.preferredTimezone ?? null,
168
+ preferredHouseStyle: this.mcpStartupDefaults.preferredHouseStyle ?? null,
169
+ weekdayLabels: this.mcpStartupDefaults.weekdayLabels ?? null,
170
+ },
171
+ ephemerisInitialized: this.isInitialized(),
172
+ stateModel: 'stateful-per-process',
173
+ };
174
+ const humanText = natalChart
175
+ ? `Server ready. Natal chart loaded: ${natalChart.name} (${natalChart.location.timezone})`
176
+ : 'Server ready. No natal chart loaded — call set_natal_chart first.';
177
+ return { data: statusData, text: humanText };
178
+ }
179
+ }
@@ -0,0 +1,37 @@
1
+ import type { EphemerisCalculator } from '../ephemeris.js';
2
+ import type { HouseCalculator } from '../houses.js';
3
+ import type { GetRisingSignWindowsInput, ServiceResult } from './service-types.js';
4
+ interface RisingSignServiceDependencies {
5
+ ephem: EphemerisCalculator;
6
+ houseCalc: HouseCalculator;
7
+ }
8
+ /**
9
+ * Internal rising-sign window scanner used by `AstroService`.
10
+ *
11
+ * @remarks
12
+ * This module owns the local-day scan, optional exact-boundary refinement, and
13
+ * serialization of sign windows while the public facade keeps the same method
14
+ * signature and result shape.
15
+ */
16
+ export declare class RisingSignService {
17
+ private readonly ephem;
18
+ private readonly houseCalc;
19
+ constructor(deps: RisingSignServiceDependencies);
20
+ /**
21
+ * Find rising-sign windows across a local calendar day.
22
+ */
23
+ getRisingSignWindows(input: GetRisingSignWindowsInput): ServiceResult<Record<string, unknown>>;
24
+ /**
25
+ * Sample the ascendant sign for a specific moment.
26
+ */
27
+ private getAscSign;
28
+ /**
29
+ * Binary-search a sign change down to a stable exact-mode boundary.
30
+ */
31
+ private refineBoundary;
32
+ /**
33
+ * Probe a scan bucket and emit every sign transition inside it.
34
+ */
35
+ private findSignTransitionsInBucket;
36
+ }
37
+ export {};
@@ -0,0 +1,137 @@
1
+ import { formatInTimezone } from '../formatter.js';
2
+ import { addLocalDays, formatLocalTimestampWithOffset, localToUTC, utcToLocal, } from '../time-utils.js';
3
+ import { ZODIAC_SIGNS } from '../types.js';
4
+ import { parseDateOnlyInput } from './date-input.js';
5
+ import { normalizeLongitude } from './shared.js';
6
+ /**
7
+ * Internal rising-sign window scanner used by `AstroService`.
8
+ *
9
+ * @remarks
10
+ * This module owns the local-day scan, optional exact-boundary refinement, and
11
+ * serialization of sign windows while the public facade keeps the same method
12
+ * signature and result shape.
13
+ */
14
+ export class RisingSignService {
15
+ ephem;
16
+ houseCalc;
17
+ constructor(deps) {
18
+ this.ephem = deps.ephem;
19
+ this.houseCalc = deps.houseCalc;
20
+ }
21
+ /**
22
+ * Find rising-sign windows across a local calendar day.
23
+ */
24
+ getRisingSignWindows(input) {
25
+ const mode = input.mode ?? 'approximate';
26
+ if (mode !== 'approximate' && mode !== 'exact') {
27
+ throw new Error(`Invalid mode: ${mode} (must be approximate or exact)`);
28
+ }
29
+ if (input.latitude < -90 || input.latitude > 90) {
30
+ throw new Error(`Invalid latitude: ${input.latitude} (must be between -90 and 90)`);
31
+ }
32
+ if (input.longitude < -180 || input.longitude > 180) {
33
+ throw new Error(`Invalid longitude: ${input.longitude} (must be between -180 and 180)`);
34
+ }
35
+ const parsed = parseDateOnlyInput(input.date);
36
+ try {
37
+ utcToLocal(new Date(), input.timezone);
38
+ }
39
+ catch {
40
+ throw new Error(`Invalid timezone: ${input.timezone}`);
41
+ }
42
+ const dayStartLocal = {
43
+ year: parsed.year,
44
+ month: parsed.month,
45
+ day: parsed.day,
46
+ hour: 0,
47
+ minute: 0,
48
+ second: 0,
49
+ };
50
+ const dayStartUtc = localToUTC(dayStartLocal, input.timezone);
51
+ const dayEndUtc = addLocalDays(dayStartLocal, input.timezone, 1);
52
+ const stepMs = mode === 'exact' ? 60 * 60 * 1000 : 2 * 60 * 60 * 1000;
53
+ const probeStepMs = mode === 'exact' ? 5 * 60 * 1000 : 30 * 60 * 1000;
54
+ const boundaries = [dayStartUtc];
55
+ let cursor = dayStartUtc;
56
+ while (cursor < dayEndUtc) {
57
+ const next = new Date(Math.min(cursor.getTime() + stepMs, dayEndUtc.getTime()));
58
+ boundaries.push(...this.findSignTransitionsInBucket(input, mode, cursor, next, probeStepMs));
59
+ cursor = next;
60
+ }
61
+ boundaries.push(dayEndUtc);
62
+ const windows = boundaries.slice(0, -1).map((start, index) => {
63
+ const end = boundaries[index + 1];
64
+ const sample = new Date((start.getTime() + end.getTime()) / 2);
65
+ const sign = this.getAscSign(input, sample).sign;
66
+ return {
67
+ sign,
68
+ start: formatLocalTimestampWithOffset(start, input.timezone),
69
+ end: formatLocalTimestampWithOffset(end, input.timezone),
70
+ durationMs: end.getTime() - start.getTime(),
71
+ };
72
+ });
73
+ const structuredData = {
74
+ date: input.date,
75
+ timezone: input.timezone,
76
+ location: {
77
+ latitude: input.latitude,
78
+ longitude: input.longitude,
79
+ },
80
+ mode,
81
+ windows,
82
+ };
83
+ const humanText = `Rising Sign Windows (${input.date}, ${input.timezone}, ${mode}):\n\n${windows
84
+ .map((window) => `${window.sign}: ${formatInTimezone(new Date(window.start), input.timezone)} → ${formatInTimezone(new Date(window.end), input.timezone)}`)
85
+ .join('\n')}`;
86
+ return {
87
+ data: structuredData,
88
+ text: humanText,
89
+ };
90
+ }
91
+ /**
92
+ * Sample the ascendant sign for a specific moment.
93
+ */
94
+ getAscSign(input, date) {
95
+ const jd = this.ephem.dateToJulianDay(date);
96
+ const houses = this.houseCalc.calculateHouses(jd, input.latitude, input.longitude, 'P');
97
+ const normalized = normalizeLongitude(houses.ascendant);
98
+ return { sign: ZODIAC_SIGNS[Math.floor(normalized / 30)], longitude: normalized };
99
+ }
100
+ /**
101
+ * Binary-search a sign change down to a stable exact-mode boundary.
102
+ */
103
+ refineBoundary(input, left, right) {
104
+ const leftSign = this.getAscSign(input, left).sign;
105
+ let lo = left;
106
+ let hi = right;
107
+ for (let i = 0; i < 25; i++) {
108
+ const mid = new Date((lo.getTime() + hi.getTime()) / 2);
109
+ const midSign = this.getAscSign(input, mid).sign;
110
+ if (midSign === leftSign) {
111
+ lo = mid;
112
+ }
113
+ else {
114
+ hi = mid;
115
+ }
116
+ }
117
+ return hi;
118
+ }
119
+ /**
120
+ * Probe a scan bucket and emit every sign transition inside it.
121
+ */
122
+ findSignTransitionsInBucket(input, mode, start, end, probeStepMs) {
123
+ const boundaries = [];
124
+ let probeCursor = start;
125
+ let currentSign = this.getAscSign(input, probeCursor).sign;
126
+ while (probeCursor < end) {
127
+ const probeNext = new Date(Math.min(probeCursor.getTime() + probeStepMs, end.getTime()));
128
+ const nextSign = this.getAscSign(input, probeNext).sign;
129
+ if (nextSign !== currentSign) {
130
+ boundaries.push(mode === 'exact' ? this.refineBoundary(input, probeCursor, probeNext) : probeNext);
131
+ }
132
+ probeCursor = probeNext;
133
+ currentSign = nextSign;
134
+ }
135
+ return boundaries;
136
+ }
137
+ }
@@ -0,0 +1,82 @@
1
+ import type { Disambiguation } from '../time-utils.js';
2
+ import type { ElectionalHouseSystem, HouseSystem } from '../types.js';
3
+ /**
4
+ * Public input type for building and caching the shared natal chart payload.
5
+ */
6
+ export interface SetNatalChartInput {
7
+ name: string;
8
+ year: number;
9
+ month: number;
10
+ day: number;
11
+ hour: number;
12
+ minute: number;
13
+ latitude: number;
14
+ longitude: number;
15
+ timezone: string;
16
+ house_system?: HouseSystem;
17
+ birth_time_disambiguation?: Disambiguation;
18
+ }
19
+ /**
20
+ * Public input type for querying natal transits.
21
+ */
22
+ export interface GetTransitsInput {
23
+ date?: string;
24
+ categories?: string[];
25
+ include_mundane?: boolean;
26
+ days_ahead?: number;
27
+ mode?: 'snapshot' | 'best_hit' | 'forecast';
28
+ max_orb?: number;
29
+ exact_only?: boolean;
30
+ applying_only?: boolean;
31
+ }
32
+ /**
33
+ * Public input type for stateless electional context lookup.
34
+ */
35
+ export interface GetElectionalContextInput {
36
+ date: string;
37
+ time: string;
38
+ timezone: string;
39
+ latitude: number;
40
+ longitude: number;
41
+ house_system?: ElectionalHouseSystem;
42
+ include_ruler_basics?: boolean;
43
+ include_planetary_applications?: boolean;
44
+ orb_degrees?: number;
45
+ }
46
+ /**
47
+ * Public input type for house lookup on an existing natal chart.
48
+ */
49
+ export interface GetHousesInput {
50
+ system?: string;
51
+ }
52
+ /**
53
+ * Public input type for daily rising-sign window lookup.
54
+ */
55
+ export interface GetRisingSignWindowsInput {
56
+ date: string;
57
+ latitude: number;
58
+ longitude: number;
59
+ timezone: string;
60
+ mode?: 'approximate' | 'exact';
61
+ }
62
+ /**
63
+ * Public output-wrapper shared by service methods that return data plus text.
64
+ */
65
+ export interface ServiceResult<T> {
66
+ data: T;
67
+ text: string;
68
+ }
69
+ /**
70
+ * Public input type for chart rendering methods.
71
+ */
72
+ export interface GenerateChartInput {
73
+ theme?: 'light' | 'dark';
74
+ format?: 'svg' | 'png' | 'webp';
75
+ output_path?: string;
76
+ }
77
+ /**
78
+ * Public input type for transit-chart rendering methods.
79
+ */
80
+ export interface GenerateTransitChartInput extends GenerateChartInput {
81
+ date?: string;
82
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import type { McpStartupDefaults } from '../entrypoint.js';
2
+ import { type HouseData, type HouseSystem, type NatalChart } from '../types.js';
3
+ /**
4
+ * Normalize any longitude into the standard 0-360 range.
5
+ *
6
+ * @param longitude - Raw longitude in degrees, including negative or >360 values
7
+ * @returns Longitude normalized into the half-open interval [0, 360)
8
+ */
9
+ export declare function normalizeLongitude(longitude: number): number;
10
+ /**
11
+ * Convert a raw longitude into zodiac sign and in-sign degree.
12
+ *
13
+ * @param longitude - Raw longitude in degrees
14
+ * @returns Sign name plus degree within that sign
15
+ *
16
+ * @remarks
17
+ * Rounded degree values that would otherwise land on 30.00 are carried into
18
+ * the next sign so serialized placement stays astrologically valid.
19
+ */
20
+ export declare function getSignAndDegree(longitude: number): {
21
+ sign: string;
22
+ degree: number;
23
+ };
24
+ /**
25
+ * Map a longitude to its house number for a resolved house table.
26
+ *
27
+ * @param longitude - Longitude to place into a house
28
+ * @param houses - Resolved house cusps for the relevant chart or moment
29
+ * @returns 1-based house number
30
+ */
31
+ export declare function getHouseNumber(longitude: number, houses: HouseData): number;
32
+ /**
33
+ * Resolve the house system precedence shared by service entrypoints.
34
+ *
35
+ * @param natalChart - Natal chart carrying stored and requested house system state
36
+ * @param startupDefaults - Process startup defaults that can provide fallback policy
37
+ * @param explicitSystem - Per-call override when the caller requested a specific system
38
+ * @returns Final house system to use for the calculation
39
+ */
40
+ export declare function resolveHouseSystem(natalChart: NatalChart, startupDefaults: Readonly<McpStartupDefaults>, explicitSystem?: string): HouseSystem;
41
+ /**
42
+ * Resolve the reporting timezone precedence shared by service entrypoints.
43
+ *
44
+ * @param startupDefaults - Process startup defaults
45
+ * @param explicitTimezone - Per-call reporting timezone override
46
+ * @param natalTimezone - Natal chart timezone used as fallback
47
+ * @returns Final reporting timezone for text and date labels
48
+ */
49
+ export declare function resolveReportingTimezone(startupDefaults: Readonly<McpStartupDefaults>, explicitTimezone?: string, natalTimezone?: string): string;
50
+ /**
51
+ * Resolve both calculation and reporting timezones for chart-based workflows.
52
+ *
53
+ * @param startupDefaults - Process startup defaults
54
+ * @param explicitReportingTimezone - Per-call reporting timezone override
55
+ * @param natalTimezone - Natal chart timezone used for local-day interpretation
56
+ * @returns Calculation timezone plus reporting timezone
57
+ *
58
+ * @remarks
59
+ * Calculation timezone controls local-day math and ephemeris lookups. Reporting
60
+ * timezone controls user-facing labels and formatted timestamps.
61
+ */
62
+ export declare function resolveTimezones(startupDefaults: Readonly<McpStartupDefaults>, explicitReportingTimezone?: string, natalTimezone?: string): {
63
+ calculationTimezone: string;
64
+ reportingTimezone: string;
65
+ };
@@ -0,0 +1,98 @@
1
+ import { ZODIAC_SIGNS } from '../types.js';
2
+ /**
3
+ * Normalize any longitude into the standard 0-360 range.
4
+ *
5
+ * @param longitude - Raw longitude in degrees, including negative or >360 values
6
+ * @returns Longitude normalized into the half-open interval [0, 360)
7
+ */
8
+ export function normalizeLongitude(longitude) {
9
+ return ((longitude % 360) + 360) % 360;
10
+ }
11
+ /**
12
+ * Convert a raw longitude into zodiac sign and in-sign degree.
13
+ *
14
+ * @param longitude - Raw longitude in degrees
15
+ * @returns Sign name plus degree within that sign
16
+ *
17
+ * @remarks
18
+ * Rounded degree values that would otherwise land on 30.00 are carried into
19
+ * the next sign so serialized placement stays astrologically valid.
20
+ */
21
+ export function getSignAndDegree(longitude) {
22
+ const normalized = normalizeLongitude(longitude);
23
+ const baseSignIndex = Math.floor(normalized / 30);
24
+ const roundedDegree = Number.parseFloat((normalized % 30).toFixed(2));
25
+ const shouldCarryToNextSign = roundedDegree >= 30;
26
+ const signIndex = shouldCarryToNextSign
27
+ ? (baseSignIndex + 1) % ZODIAC_SIGNS.length
28
+ : baseSignIndex;
29
+ return {
30
+ sign: ZODIAC_SIGNS[signIndex],
31
+ degree: shouldCarryToNextSign ? 0 : roundedDegree,
32
+ };
33
+ }
34
+ /**
35
+ * Map a longitude to its house number for a resolved house table.
36
+ *
37
+ * @param longitude - Longitude to place into a house
38
+ * @param houses - Resolved house cusps for the relevant chart or moment
39
+ * @returns 1-based house number
40
+ */
41
+ export function getHouseNumber(longitude, houses) {
42
+ const normalized = normalizeLongitude(longitude);
43
+ for (let house = 1; house <= 12; house++) {
44
+ const start = normalizeLongitude(houses.cusps[house]);
45
+ const nextHouse = house === 12 ? 1 : house + 1;
46
+ const end = normalizeLongitude(houses.cusps[nextHouse]);
47
+ const span = (end - start + 360) % 360;
48
+ const offset = (normalized - start + 360) % 360;
49
+ if (span === 0 || offset === 0 || offset < span) {
50
+ return house;
51
+ }
52
+ }
53
+ return 12;
54
+ }
55
+ /**
56
+ * Resolve the house system precedence shared by service entrypoints.
57
+ *
58
+ * @param natalChart - Natal chart carrying stored and requested house system state
59
+ * @param startupDefaults - Process startup defaults that can provide fallback policy
60
+ * @param explicitSystem - Per-call override when the caller requested a specific system
61
+ * @returns Final house system to use for the calculation
62
+ */
63
+ export function resolveHouseSystem(natalChart, startupDefaults, explicitSystem) {
64
+ return (explicitSystem ||
65
+ natalChart.requestedHouseSystem ||
66
+ startupDefaults.preferredHouseStyle ||
67
+ natalChart.houseSystem ||
68
+ 'P');
69
+ }
70
+ /**
71
+ * Resolve the reporting timezone precedence shared by service entrypoints.
72
+ *
73
+ * @param startupDefaults - Process startup defaults
74
+ * @param explicitTimezone - Per-call reporting timezone override
75
+ * @param natalTimezone - Natal chart timezone used as fallback
76
+ * @returns Final reporting timezone for text and date labels
77
+ */
78
+ export function resolveReportingTimezone(startupDefaults, explicitTimezone, natalTimezone) {
79
+ return explicitTimezone ?? startupDefaults.preferredTimezone ?? natalTimezone ?? 'UTC';
80
+ }
81
+ /**
82
+ * Resolve both calculation and reporting timezones for chart-based workflows.
83
+ *
84
+ * @param startupDefaults - Process startup defaults
85
+ * @param explicitReportingTimezone - Per-call reporting timezone override
86
+ * @param natalTimezone - Natal chart timezone used for local-day interpretation
87
+ * @returns Calculation timezone plus reporting timezone
88
+ *
89
+ * @remarks
90
+ * Calculation timezone controls local-day math and ephemeris lookups. Reporting
91
+ * timezone controls user-facing labels and formatted timestamps.
92
+ */
93
+ export function resolveTimezones(startupDefaults, explicitReportingTimezone, natalTimezone) {
94
+ return {
95
+ calculationTimezone: natalTimezone ?? 'UTC',
96
+ reportingTimezone: resolveReportingTimezone(startupDefaults, explicitReportingTimezone, natalTimezone),
97
+ };
98
+ }
@@ -0,0 +1,48 @@
1
+ import type { EclipseCalculator } from '../eclipses.js';
2
+ import type { McpStartupDefaults } from '../entrypoint.js';
3
+ import type { EphemerisCalculator } from '../ephemeris.js';
4
+ import type { RiseSetCalculator } from '../riseset.js';
5
+ import { type NatalChart } from '../types.js';
6
+ import type { ServiceResult } from './service-types.js';
7
+ interface SkyServiceDependencies {
8
+ ephem: EphemerisCalculator;
9
+ riseSetCalc: RiseSetCalculator;
10
+ eclipseCalc: EclipseCalculator;
11
+ mcpStartupDefaults: Readonly<McpStartupDefaults>;
12
+ now: () => Date;
13
+ formatTimestamp: (date: Date, timezone: string) => string;
14
+ }
15
+ /**
16
+ * Internal current-sky and runtime lookup workflow used by `AstroService`.
17
+ *
18
+ * @remarks
19
+ * This module owns read-only runtime lookups that depend on "now", including
20
+ * retrogrades, asteroid/node snapshots, rise/set tables, and eclipse queries.
21
+ */
22
+ export declare class SkyService {
23
+ private readonly ephem;
24
+ private readonly riseSetCalc;
25
+ private readonly eclipseCalc;
26
+ private readonly mcpStartupDefaults;
27
+ private readonly now;
28
+ private readonly formatTimestamp;
29
+ constructor(deps: SkyServiceDependencies);
30
+ /**
31
+ * Return the currently retrograde planets for the requested reporting timezone.
32
+ */
33
+ getRetrogradePlanets(timezone?: string): ServiceResult<Record<string, unknown>>;
34
+ /**
35
+ * Return the next rise and set events after the local day anchor for the chart location.
36
+ */
37
+ getRiseSetTimes(natalChart: NatalChart): Promise<ServiceResult<Record<string, unknown>>>;
38
+ /**
39
+ * Return current asteroid and node positions for the requested reporting timezone.
40
+ */
41
+ getAsteroidPositions(timezone?: string): ServiceResult<Record<string, unknown>>;
42
+ /**
43
+ * Look up the next solar and lunar eclipses after the current instant.
44
+ */
45
+ getNextEclipses(timezone?: string): ServiceResult<Record<string, unknown>>;
46
+ private getDateLabel;
47
+ }
48
+ export {};