NASA Adapter — High-Level Design
Reading guide: The current package architecture defines the binder boundary. Sections below describe reference designs unless an implementation note says otherwise. Standalone facade, cache and provenance services are dropped; their integration responsibilities belong to the consumer.
Status: Draft for review · Part of: Astro Binder · Updated: 2026-10-24
The NASA adapter is the only component that knows JPL Horizons. It builds a query, guards the call, verifies the response signature, parses the text table into typed values and maps targets to canonical ids; it returns no houses, no ayanamsa and no sidereal positions.
1. Purpose
Horizons is the binder’s independent reference, not a second engine of record. It answers “where was this body at this instant according to JPL”. The API returns a fixed-width text table inside JSON, with no astrology primitives.
The adapter contains that awkwardness:
- query parameters come from templates, never ad hoc assembly;
- the polite-citizen rules (timeout, concurrency cap, backoff, contact user agent) live in one place;
- the response shape is verified before any number is read, so a format change fails loudly;
- parsing rules are pinned to the documented API version, not to whatever arrives.
2. Where it sits
drift validator ────┐
├──▶ nasa adapter ──▶ JPL Horizons API
coverage fallback ──┘ │
▼
parsed rows + declaration ──▶ canonical normalizer
- Used by: the drift validator for every comparison, and the explicit coverage fallback when Swiss cannot serve a body or date.
- Downstream: the canonical normalizer applies the ayanamsa for sidereal answers, because Horizons cannot.
- Never: runs on the user’s request path in v1, selects itself, or produces houses.
3. Responsibilities
| Stage | Owns | Refuses to do |
|---|---|---|
| Build the query | COMMAND, CENTER, TLIST, QUANTITIES, TIME, ANG_FORMAT, time scale |
Ask for houses, ayanamsa or sidereal output |
| Guard the call | Timeout, concurrency cap, backoff, descriptive user agent with contact | Retry a 4xx or hammer a throttled endpoint |
| Fetch and verify | HTTPS GET, JSON decode, signature.version and result shape |
Parse an unrecognised layout into plausible zeros |
| Parse the table | CSV rows → typed numbers, columns located by header name | Assume a column order or a column count |
| Map to canonical | Horizons target ids → canonical ids, AU and AU/day, epoch buckets | Return a body it has no mapping for |
4. Architecture
Diagram: reference design, not deployed behaviour.
Query templates and the response contract are dependencies, not stages: both are pinned data, so changing either is a contract change.
5. Rules
- Sole owner of Horizons. Parameter names, the base URL and
signature.versionappear only inadapter/nasa; new quantities are added here. - Ask for exactly what is compared.
TIMEandTLISTset the scale and instant explicitly; never a server default for TT or epoch. - Verify before parse. A missing column, an absent
$$SOE/$$EOEpair or an unexpectedsignature.versionis aPROVIDER_RESPONSEerror; nothing is zero-filled. - No astrology, ever. Horizons has no houses, ayanamsa (the tropical-to-sidereal offset) or sidereal zodiac. A sidereal request is converted downstream; a houses request is refused by capability, not attempted here.
- Privacy-safe by default. Queries are geocentric unless a caller asks otherwise, and validation samples never carry a user’s birth coordinates.
- Polite and bounded. One timeout, bounded concurrency, exponential backoff on throttling and 5xx, and no retry on a 4xx.
- Off the request path. In v1 the adapter serves validation and the declared fallback; it adds no latency to a normal calculation.
6. Edge cases
| Case | Behaviour |
|---|---|
| Body unsupported for the requested span | Case skipped and counted as unsupported; no alert |
| Throttled or repeated 5xx | Breaker opens; the run is marked incomplete rather than retried in a loop |
| Column header present with an empty cell | Parse error with the column name, never zero |
signature.version changed |
Typed error naming the received and expected version |
$$SOE/$$EOE markers absent |
PROVIDER_RESPONSE; no partial rows returned |
| Query exceeds its deadline | PROVIDER_TIMEOUT; no partially parsed result is used |
| Target designation instead of an id | Resolved once per body and kept in the adapter’s small target map |
| Text table width changes | Pinned-example contract test fails before deployment |
| Topocentric request from a live sample | Coordinates stripped; the case is re-run geocentric |
7. Out of scope
- Choosing or falling back between providers (router).
- Deciding whether a delta is drift, and alerting (drift validator).
- Applying ayanamsa and normalising units and frame (normalizer).
- Houses and sidereal definitions, which Horizons does not provide.
8. Code
The @astrology/nasa-adapter workspace package depends on the implemented standalone
@astrology/adapter-contract, just as the
Swiss package does. It implements EphemerisAdapter without copying types or importing Swiss.
Capabilities advertise only native features and zodiacs; houses and native sidereal requests
reject with the shared UNSUPPORTED_FEATURE code. Nonapplicable binding versions, tidal-model
values and native flags can be null. Hosts own HTTP mappings; the package does not host a server.
The module-level design is in design.md.
| File | Responsibility |
|---|---|
src/nasa/horizons-adapter.ts |
Stage orchestration, declaration and the port contract |
src/nasa/query-builder.ts |
Pinned parameter template per body, centre and instants |
src/nasa/response.ts |
Envelope verification and header-located CSV parsing |
src/nasa/target-map.ts |
Canonical ids ↔ Horizons target ids and speed steps |
src/nasa/http-client.ts |
Timeout, concurrency, backoff, breaker and transport error codes |