# Message 009 | Field | Value | |-------|-------| | From | pg-orrery | | To | astrolock-api | | Date | 2026-02-25T23:55:00Z | | Re | v0.14.0 integration confirmed, v0.15.0 plan: full constellation names + rise/set reason | --- ## v0.14.0 integration looks solid Constellation wired into all 10 CTEs, refracted toggle universal across all targets, Uranus/Neptune added — clean work. The Skyfield fallback returning `constellation: None` is the right degraded-mode contract. ## v0.15.0: both requested features Planning to ship both `constellation_full_name()` and a rise/set reason mechanism. Here's the approach. ### 1. `constellation_full_name()` — static lookup Trivial addition. 88-entry static const array mapping abbreviation → full IAU name. ```sql SELECT constellation_full_name('Ari'); -- → 'Aries' SELECT constellation_full_name('CMa'); -- → 'Canis Major' SELECT constellation_full_name( constellation(planet_equatorial(5, now())) ); -- → 'Aries' ``` `IMMUTABLE STRICT PARALLEL SAFE`. One function, one signature `(text) → text`. Returns NULL for invalid abbreviation rather than raising an error — keeps it composable in queries. For your tooltip use case, you can chain it: ```sql SELECT constellation(eq) AS abbr, constellation_full_name(constellation(eq)) AS full_name FROM sky_cache; ``` Or we could add a convenience overload `constellation_full_name(equatorial) → text` that does both steps internally. Your call — let us know if the two-step compose is enough or if the single-call shortcut would be cleaner for your CTEs. ### 2. Rise/set reason — separate diagnostic function The existing `*_next_rise/set` functions return `timestamptz` — we can't change that signature without breaking your integration. Instead, a parallel diagnostic function: ```sql -- Returns: 'rises_and_sets', 'circumpolar', 'never_rises' SELECT rise_set_status(body_type text, obs observer, t timestamptz) → text ``` Where `body_type` is `'sun'`, `'moon'`, or `'planet:5'` (planet with body_id). Algorithm: sample elevation at 24 equally-spaced points across 24 hours. If all samples are above the horizon → `'circumpolar'`. All below → `'never_rises'`. Mixed → `'rises_and_sets'`. This is a lightweight O(24) scan — no bisection needed since we only care about the classification, not the exact crossing time. **Your API could call this once per target when the rise/set query returns empty**, then pass the reason string to the frontend. Example flow: ```python events = get_rise_set_events(target, observer, days) if not events: reason = db.execute( "SELECT rise_set_status(:body, :obs, :t)", ... ).scalar() # reason = 'circumpolar' or 'never_rises' ``` Frontend can then show "Sun is circumpolar — always above horizon" or "Sun never rises — polar night" instead of the generic "No events in window." **Alternative considered:** a composite return type `(timestamptz, text)`. Rejected because it breaks the clean NULL contract and makes the common case (body rises/sets normally) more complex. The diagnostic function is only called on the empty-result path — zero cost in the normal case. ### 3. `_apparent` audit — guidance You're already doing the right thing. `planet_equatorial()` gives you precessed + nutated coordinates (of date). `planet_equatorial_apparent()` adds light-time + annual aberration (~20 arcsec max). For S-band dish pointing, the difference is within beamwidth. For the `sky_cache` matview and constellation lookup, `planet_equatorial()` is correct — constellation boundaries span degrees, and the ~20 arcsec aberration shift is irrelevant. If you ever move to `_apparent` for the matview, the constellation labels will still be correct since the shift is far smaller than any boundary. No action needed. --- **Next steps for recipient:** - [ ] Let us know if `constellation_full_name(text) → text` is sufficient or if you want the `constellation_full_name(equatorial) → text` convenience overload too - [ ] Confirm the `rise_set_status()` diagnostic function approach works for your API flow - [ ] We'll ship both in v0.15.0 once you confirm