Internal module guide. This module is now part of
@astrology/ephemeris; use the public package README for application setup.
ephemeris-binder module
@astrology/ephemeris assembles the existing Swiss and NASA adapters into one reusable
Node.js package for Vedic and Western consumers. Both consumers call the same methods; requests
select zodiac, ayanamsa, bodies, speed and house conventions. Swiss is the default primary for both.
The package starts no server, scheduler, persistent cache or drift job.
Create and use
Declare "@astrology/ephemeris/node": "workspace:*" in the consumer’s dependencies. Run
pnpm setup:ephemeris once to provision the pinned Swiss files, then pnpm build:ephemeris.
import { createBinder, PositionQuery, resolveHouse } from '@astrology/ephemeris/node';
// Supply your host's tested UTC-to-TT resolver. Do not treat UTC JD as TT or use a fixed ΔT.
const binder = await createBinder({
resolveJulianDayTt: epochMilliseconds => timeService.julianDayTt(epochMilliseconds),
nasa: { userAgent: 'my-chart-app/1.0 (ops@example.com)' },
});
const vedic = await binder.positions(
PositionQuery.at('2000-01-01T12:00:00Z', { zodiac: 'sidereal', ayanamsa: 'lahiri' })
.forBodies('sun', 'moon').withSpeed().build(),
);
const western = await binder.positions({
instant: '2000-01-01T12:00:00Z', zodiac: 'tropical', bodies: ['sun', 'moon'], includeSpeed: true,
});
const houses = await binder.houses({
instant: '2000-01-01T12:00:00Z', zodiac: 'tropical',
observer: { latitude: 19.076, longitude: 72.8777 }, system: 'whole_sign',
});
const sunHouse = resolveHouse(western.data.positions[0]!.longitude, houses.data.cusps);
timeService above is a host dependency, not an exported Binder service. The resolver receives
validated integer epoch milliseconds before routing and returns the Terrestrial Time (TT) Julian Day, a continuous day count on the uniform clock used for planetary positions, for that exact
instant. Adapter calculations still use their own time models and return those declarations.
The Binder does not force provider clocks to agree or claim a new accuracy estimate. Civil timezone
resolution also stays with the host: requests require an offset-bearing timestamp or epoch value.
Reuse one Binder so adapter concurrency limits and NASA’s breaker remain shared. createBinder
constructs exactly two adapters and probes both before returning. Swiss probes the native engine
and verified files; NASA’s capability probe is local and does not establish remote availability.
Either probe rejecting rejects initialization. For injected adapters, use
AstroBinder.create({ swiss, nasa }, options) with the same options except concrete provider config.
Injected ids must be swiss-ephemeris and nasa-horizons.
Shared result contract
positions() preserves normalized data, declaration and provenance, then adds decision and
semantics. semantics.positions retains requested body order and includes sign/degree, motion,
and sidereal nakshatra/pada (lunar-mansion segment and its quarter). Tropical nakshatra is null; missing speed remains unknown. No second
ayanamsa subtraction happens. semantics.aspects lists aspect matches once per unordered body pair using
the existing major-aspect defaults; set aspectProfile to explicit rules or [] to disable aspects.
The result records SEMANTICS_VERSION; retain your custom profile identity with saved results.
houses() returns normalized cusps/angles, provenance and the provider decision. The re-exported
resolveHouse uses the existing semantics rule. Callers must match instant, observer and zodiac
when combining positions and houses; this pure helper cannot verify that context from numbers.
The Binder does not create vargas, dashas, panchang, Western chart rules or interpretation.
capabilities() returns detached snapshots for both providers, rather than suggesting they have
identical support. Recreate the Binder to refresh snapshots. Provider/engine/binding/ephemeris/file
identity changes during execution reject with INTERNAL, requiring refreshed initialization.
Provider policy and failures
Each request is validated before invoking the time resolver or either provider. One router decision selects a complete request; no body-level merging occurs. Provider errors and host hook failures propagate unchanged. There is no Binder retry or switch to NASA after a Swiss timeout or outage. The adapters retain their own deadlines, retry policies and native resource limits.
NASA supports tropical positions only; houses, sidereal and lunar nodes cannot fall back to it. To allow eligible tropical coverage fallback, explicitly configure:
routing: {
coverageFallbacks: [{ provider: 'nasa-horizons', bodies: ['sun', 'moon'] }],
}
The default Swiss and NASA adapters advertise the same conservative date range, so this does not
extend their installed coverage automatically. routing.primaryProvider can select NASA for a
dedicated tropical consumer, subject to its capabilities and topocentric (observer-location) privacy option.
Optional routing gates remain read-only admission hooks. Even a positive cacheHas result does
not return cached data: the Binder still executes the admitted adapter. decision.cacheKey
contains request data and should not be logged. Storage, quota reservation, HTTP error mapping,
licensing, logging configuration and drift scheduling remain host responsibilities.
Verification
Run pnpm check:ephemeris and pnpm test:ephemeris. Tests exercise real routing, normalization and
semantics against controlled adapters, plus the concrete factory with real Swiss native calls and
NASA’s real capability probe. The suite makes no live NASA HTTP requests and requires the pinned
Swiss files; failures are never silently skipped. See the Binder architecture.
Native solar events
binder.riseSet(request) returns Hindu sunrise, next sunrise and optional sunset through the
same router, worker isolation, identity checks and normalizer as other Binder reads.
const result = await binder.riseSet({
instant: '2000-01-01T12:00:00Z',
end: '2000-01-04T12:00:00Z',
observer: { latitude: 19.076, longitude: 72.8777 },
zodiac: 'tropical',
definition: 'hindu',
includeSunset: true,
});
// result.data contains integer UTC epoch milliseconds or explicit null no-event values.
The explicit search window is [instant, end), at most 72 hours. Sunset is the first setting after the first sunrise (or after the start if sunrise is absent). A next sunrise outside the window is null; no 24-hour day is invented. Both endpoints must fit the installed Swiss coverage. Only the Swiss Hindu definition is supported: disc centre, no refraction, geocentric Sun, with solar ecliptic latitude ignored.
Swiss advertises rise-set and riseSetDefinitions: ['hindu']; NASA does not.
Events use the explicit observer and declare origin: 'observer-events'. The result carries its
own source, normalizer and routing evidence. Native UTC conversion rounds once to milliseconds;
the normalizer validates these timestamps without applying angular rounding.
The real Composer test now verifies the complete native chart/event path. Its recorded TT resolver serves one fixture instant; production hosts still own a tested resolver for arbitrary birth instants. No live NASA request is required.
Derived reads
Three reads take an ordinary PositionsRequest, route one position read and derive from it, so
they share its declaration, provenance and routing decision. No provider change is involved.
| Read | Returns per body | Derivation |
|---|---|---|
binder.declinations(request) |
declination (angle north/south of the celestial equator) in degrees, plus obliquity | Tropical apparent longitude/latitude rotated by the true obliquity of date (IAU mean obliquity with the leading nutation terms (nutation: the short periodic wobble of the Earth’s axis), Meeus ch. 22). Sidereal reads add the declared ayanamsa back first, so the value is zodiac-independent. |
binder.meanLongitudes(request) |
meanLongitude (madhya), seeghrocca |
Mean elements of date at the read’s TT (Meeus table 31.A; Moon from Meeus 47.1), in the request zodiac. Mars, Jupiter, Saturn: own mean longitude, seeghrocca the mean Sun. Mercury, Venus: the mean Sun, seeghrocca their own. Sun, Moon: no seeghrocca. Other bodies: both null. |
binder.discDiameters(request) |
diameterArcsec |
Apparent angular diameter from IAU mean radii and the returned distance; null for nodes. |
A sidereal read without a declared ayanamsa offset fails with PROVIDER_RESPONSE. The Binder also
re-exports resolveSphutaDrishti for degree-based aspect values. The runtime reader adds
declinations, meanLongitudes and discDiameters (context-shaped, with null bodies dropped)
whenever the Binder has them, so a Vedic service given that reader opens the matching gates.