---
date: 2026-05-29 07:57 -04:00
---

**Coordinate & time foundation**

1. **eqToHor** **/** **horToEq** **sign bug** (Const) — the rotation in each function was being driven by the other's angle. Swap to use each transform's own angle. Symptom: every altitude/azimuth and ascension computation was geometrically wrong.

2. **daystring** **integer arithmetic** (Const) — rewrite from float math to seconds-based integer division so year/month/day and HH:MM are computed correctly.

3. **OST +0.5-day shift in** **daystring** (Const) — engine t = 0 is _solar noon_ at longitude 0, not midnight. Add + 0.5 before splitting into y/m/d/h/m so OST 00:00 lines up with solar midnight and OST 12:00 with solar noon at the prime meridian.

4. **Local apparent solar time** (Const.localTimeString(t, longitude)) — new helper: LST = siderealTime(t) + longitude, HA = LST − Sun.ra(t), localHours = 12 + HA · 12/π. Round in _total minutes_ (so 11.99999988 → 12:00, not 11:60), then mod 1440. This is what produces the "(local HH:MM)" annotation distinct from OST.

5. **Arc** **negative minutes/seconds** (Arc) — floating-point rounding near exact degree/minute boundaries was producing negative mm or ss. Clamp intermediates to zero.

**Horoscope math**

6. **Ascendant formula** (Horoscope) — was computing the ascendant by converting a zenith vector to the ecliptic, which gives the wrong ecliptic longitude. Replace with the standard ecliptic-horizon intersection formula using LST, latitude, and ecliptic obliquity.

7. **Descending point** (Horoscope) — descending was being computed as −ascendant.ec.phi. Correct: ascendant.ec.phi + π.

8. **Newton-iteration solar-time finder** (Horoscope.findSolarTime) — old findLocalTime used bisection on the Sun's _altitude_. Replace with Newton iteration on the Sun's _hour angle_ (target HA = (hour − 12)·π/12). Converges in 1–2 steps and works correctly at high latitudes / near solstices.

**Star map**

9. **Compass labels** (StarMap) — N/S and E/W were swapped on the corner compass. Fix to S top, N bottom, E left, W right (looking-up-at-the-sky convention).

**Sunsign / zodiac boundaries**

10. **Zodiac boundary longitudes** (Const.sunsign / sunSignInt / sunSignDescription) — were evenly-spaced approximations. Replace each threshold with the Sun's ecliptic longitude at midnight on the canonical first day of that sign in **year 720 TR**. This makes the function agree with the rulebook for the current campaign era. The world's slight solar drift (360.0011-day year) carries the rulebook calendar dates ~1 day forward per 360 years; that's intentional in the model.

**Solar / lunar constants (canon)**

11. **Sun.rotationPerDay** — replace the hand-rolled approximation 0.0027777692901493913 with the exact expression 1 / Sun.solarPeriod (= 1 / 360.0011). Eliminates a small accumulated phase error that grew over centuries.

12. **Moon.synodicPeriod = 29.999985** (canon) — new named constant for the lunation length.

13. **Moon.rotationPerDay** — derive from canon: Moon.rotationPerDay = 1 / Moon.synodicPeriod + Sun.rotationPerDay. Follows from synodicRate = Moon − Sun.

14. **Moon.initLongitude** — pin so the first full moon of Nuzyael (month 1) of year **720 TR** lands at OST day 15 noon.

Formula:

```
anchor = makeDate(720, 1, 15, 12, 0, 0) − 0.5   // = 258854.0 engine days
    raw    = Sun.initLongitude + 0.5 − anchor / Moon.synodicPeriod
    Moon.initLongitude = raw mod 1   // normalize to [0,1)
```

Reasoning: at any t, Moon - Sun ≡ (Moon.init − Sun.init) + t / synodicPeriod (mod 1); setting this ≡ 0.5 at t = anchor gives the formula above. With the canon 29.999985-day lunation, full moons drift ~0.0018 day/year forward of the 15th — intentional per lore.

**New capability: precise syzygy times**

15. **Syzygy.nextFullMoon(after:)****,** **nextNewMoon(after:)****,** **nextSyzygy(after:target:)** — bracket-and-bisect on f(t) = wrap(Moon.ecLong(t) − Sun.ecLong(t) − target) reduced to (−π, π]. Bracket step = 1 day (safe: synodic motion ≈ 12°/day = 0.21 rad/day, never wraps a full circle in one step). The wrapped function jumps from +π to −π once per synodic month at the _opposite_ syzygy; the bracket condition fLo ≤ 0 && fHi ≥ 0 && fHi > fLo rejects that wrap so only true zero-crossings are returned. Bisect to ~1e-4 days (~8.6 s).