Canonical Normalizer — 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 normalizer converts one provider answer into the canonical contract: canonical identifiers, units and frame, exactly one zodiac conversion, one rounding point and a provenance block. It is pure code, with no I/O, no provider choice and no astrology.
The workspace implementation now normalizes the shared adapter contract and provides an injected adapter wrapper. It validates declarations, wraps angles, rounds output once and records provenance. Raw-unit/frame transforms and normalizer-side ayanamsa calculation below remain planned; current adapters already return canonical units/frame and apply sidereal offsets themselves.
1. Purpose
Swiss Ephemeris returns typed arrays with flags; Horizons returns a text table inside JSON. Neither shape may leak past the provider edge, and neither means the same thing by default. The normalizer resolves them into one shape and one meaning for every downstream calculation.
2. Where it sits
provider adapter ──▶ canonical normalizer ──▶ astrology semantics
- Upstream: the Swiss or NASA adapter returns raw values plus a declaration of frame, units and time scales.
- Downstream: semantics derives signs and nakshatras (lunar mansions); the facade assembles the response.
- Never: selects a provider, caches, retries or reads the clock.
3. Responsibilities
| Stage | Owns | Refuses to do |
|---|---|---|
| Admit | Provider status and error mapping, required fields, NaN/range checks | Coerce a missing value into zero |
| Map identifiers | Provider body ids → canonical ids; km→AU, km/s→AU/day, angles in degrees | Guess an unknown body |
| Resolve frame | Provider-declared frame → canonical apparent ecliptic-of-date, airless | Mix two frames in one result |
| Apply zodiac once | Provider-native sidereal or binder-side ayanamsa subtraction | Subtract an ayanamsa twice |
| Validate, round, stamp | Range checks, single rounding point, precision and provenance | Claim precision the engine cannot provide |
| Shared tables | Transform library and precision rules | Contain provider-specific branches |
4. Architecture
Diagram: reference design, not deployed behaviour.
The two shared cards are deliberate: more than one stage uses frame math and precision rules, so neither may live inside an adapter. Stage order is the contract’s order of operations, not a request sequence.
5. Rules
- One conversion per property. Convert provider units to canonical units once; later stages may validate but never re-scale.
- One ayanamsa application. Swiss can return sidereal longitudes natively; Horizons cannot.
Whoever applies the ayanamsa (the tropical-to-sidereal offset) is recorded in
ayanamsaAppliedBy(providerornormalizer), with the ayanamsa value and mode. - One rounding point. Full precision is carried internally; rounding happens at serialisation and is stated in provenance.
- Unknown values fail. An unknown body, house system or node type is a typed error, never a silent default.
- No silent clamping. A longitude outside 0–360 is wrapped explicitly and recorded; a latitude or distance outside its range is rejected.
- One provider per result. The shape carries a single provenance block by construction.
- Determinism. Same provider output and engine version give byte-identical canonical values.
6. Edge cases
| Case | Behaviour |
|---|---|
| Moshier fallback used | Result returned with reduced precisionArcsec and degraded: true |
| Horizons table column mismatch | Provider-response error with the API version; never zero-filled columns |
| Longitude exactly 360° | Normalised to 0° and recorded as a wrap |
| Speed absent from provider output | speed: null; the module does not differentiate numerically |
| Sidereal request on Horizons | Normalizer applies the ayanamsa; houses remain unsupported |
| Provider returns an unknown body id | Typed error naming the adapter, not a dropped body |
| Cache hit | Stored canonical values are returned unchanged; no re-normalisation |
7. Out of scope
- Choosing or falling back between providers (router).
- Deriving signs, nakshatras, aspects or house placement (semantics).
- Caching and provenance storage (cache and provenance store).
- Protocol handling such as HTTP retries, timeouts and response parsing (adapters).
8. Planned code
| Piece | Responsibility |
|---|---|
normalizer/CanonicalNormalizer |
Stage orchestration and result assembly |
normalizer/BodyMapper |
Provider body ids ↔ canonical ids |
normalizer/UnitMapper |
km, km/s, angles → AU, AU/day, degrees |
normalizer/FrameResolver |
Declared frame → canonical frame |
normalizer/ZodiacResolver |
Native or binder-side ayanamsa, applied-once guard |
normalizer/ProvenanceStamper |
Engine, files, flags, time scales, precision |