Packages6 MIN READ72 / 116

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.