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.
- package/README.md +15 -5
- package/dist/astro-service/chart-output-service.d.ts +44 -0
- package/dist/astro-service/chart-output-service.js +110 -0
- package/dist/astro-service/date-input.d.ts +14 -0
- package/dist/astro-service/date-input.js +30 -0
- package/dist/astro-service/electional-service.d.ts +45 -0
- package/dist/astro-service/electional-service.js +305 -0
- package/dist/astro-service/natal-service.d.ts +41 -0
- package/dist/astro-service/natal-service.js +179 -0
- package/dist/astro-service/rising-sign-service.d.ts +37 -0
- package/dist/astro-service/rising-sign-service.js +137 -0
- package/dist/astro-service/service-types.d.ts +82 -0
- package/dist/astro-service/service-types.js +1 -0
- package/dist/astro-service/shared.d.ts +65 -0
- package/dist/astro-service/shared.js +98 -0
- package/dist/astro-service/sky-service.d.ts +48 -0
- package/dist/astro-service/sky-service.js +144 -0
- package/dist/astro-service/transit-service.d.ts +82 -0
- package/dist/astro-service/transit-service.js +353 -0
- package/dist/astro-service.d.ts +101 -89
- package/dist/astro-service.js +162 -1042
- package/dist/tool-registry.js +1 -1
- package/docs/product/architecture-boundaries.md +8 -0
- package/docs/releases/1.3.0.md +51 -0
- package/docs/releases/README.md +17 -0
- package/package.json +4 -1
- package/src/astro-service/chart-output-service.ts +155 -0
- package/src/astro-service/date-input.ts +40 -0
- package/src/astro-service/electional-service.ts +395 -0
- package/src/astro-service/natal-service.ts +235 -0
- package/src/astro-service/rising-sign-service.ts +181 -0
- package/src/astro-service/service-types.ts +90 -0
- package/src/astro-service/shared.ts +128 -0
- package/src/astro-service/sky-service.ts +191 -0
- package/src/astro-service/transit-service.ts +507 -0
- package/src/astro-service.ts +177 -1386
- package/src/tool-registry.ts +1 -1
- package/tests/README.md +15 -0
- package/tests/property/electional-service.property.test.ts +67 -0
- package/tests/property/helpers/arbitraries.ts +126 -0
- package/tests/property/helpers/config.ts +52 -0
- package/tests/property/helpers/runtime.ts +12 -0
- package/tests/property/houses.property.test.ts +74 -0
- package/tests/property/rising-sign-service.property.test.ts +255 -0
- package/tests/property/service-transits.property.test.ts +154 -0
- package/tests/property/time-utils.property.test.ts +91 -0
- package/tests/property/transits.property.test.ts +113 -0
- package/tests/unit/astro-service/chart-output-service.test.ts +102 -0
- package/tests/unit/astro-service/electional-service.test.ts +182 -0
- package/tests/unit/astro-service/natal-service.test.ts +126 -0
- package/tests/unit/astro-service/rising-sign-service.test.ts +145 -0
- package/tests/unit/astro-service/sky-service.test.ts +130 -0
- package/tests/unit/astro-service/transit-service.test.ts +312 -0
- package/tests/unit/astro-service.test.ts +136 -781
- package/tests/unit/rising-sign-windows.test.ts +93 -0
- package/tests/unit/tool-registry.test.ts +11 -0
- package/tests/validation/README.md +14 -0
- package/tests/validation/adapters/internal.ts +234 -4
- package/tests/validation/compare/electional.ts +151 -0
- package/tests/validation/compare/rising-sign-windows.ts +347 -0
- package/tests/validation/compare/service-transits.ts +205 -0
- package/tests/validation/fixtures/electional/core.ts +88 -0
- package/tests/validation/fixtures/rising-sign-windows/core.ts +57 -0
- package/tests/validation/fixtures/service-transits/core.ts +89 -0
- package/tests/validation/utils/fixtureTypes.ts +139 -1
- 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 {};
|