DEV Community

Svarbhanu Neel
Svarbhanu Neel

Posted on

I built a zero-dependency sky engine in TypeScript, verified to 4.6 arcseconds

Today I shipped v0.1.0 of grahan, an MIT-licensed TypeScript library that computes Sun/Moon positions, sunrise/sunset, moon phases, and the Vedic panchang (tithi, nakshatra, yoga, karana, Rahu Kaal). Zero runtime dependencies. Runs in Node 18+, browsers, and edge runtimes.

npm install @grahan/vedic
Enter fullscreen mode Exit fullscreen mode
import { panchang } from '@grahan/vedic';

const p = panchang({
  date: new Date('2026-07-02T06:00:00Z'),
  latitude: 27.7172,
  longitude: 85.324,
  timezone: 'Asia/Kathmandu',
});
// p.tithi      → { index: 17, paksha: 'krishna', name: 'Tritiya' }
// p.nakshatra  → { index: 21, name: 'Shravana', pada: 1 }
// p.rahuKaal   → 13:51 to 15:35 local, that Thursday
Enter fullscreen mode Exit fullscreen mode

This post is about the three engineering decisions that mattered, and the bugs the process caught that I would never have found by eyeballing outputs.

Why zero dependencies

The industry-standard ephemeris is Swiss Ephemeris. It is superb, but AGPL (or paid), so linking it was off the table for an MIT library. The alternatives were: wrap a WASM build (license problem remains), depend on a chain of npm astronomy packages of varying maintenance, or implement from the public-domain sources: Meeus's Astronomical Algorithms, the VSOP87 planetary theory, and the ELP-2000 lunar series.

I implemented from sources. It also bought me something I didn't expect to value so much: the whole library is auditable. Every formula has a citation to a chapter of Meeus or an official data file.

Rule one: generate the answer sheet before writing the formula

Swiss Ephemeris is AGPL code, but its outputs are facts. So before writing any calculation, I ran an offline Python script (pyswisseph) that generated JSON reference fixtures and committed them to the repo: 120 Sun/Moon longitudes spanning 1900 to 2100, 150 sunrise/sunset events across 5 sites from Singapore to Utqiagvik at 71°N, 41 ayanamsa epochs. Tests read the JSON and assert tolerances, and CI prints an accuracy report on every run:

[accuracy] sun apparent longitude: n=120 max=4.60″ mean=0.85″
[accuracy] moon apparent longitude: n=120 max=65.37″ mean=10.46″
[accuracy] sunrise/sunset: n=272 max=4.6s mean=0.9s polar-states=28
Enter fullscreen mode Exit fullscreen mode

Promised tolerances are ±36″ (Sun), ±180″ (Moon), ±60 s (rise/set). Measured is 8 to 13 times inside the promise. The README publishes both columns, promise and measurement, because accuracy claims without numbers are marketing.

Rule two: don't type coefficient tables by hand

The Sun's position needs a truncated VSOP87 series: hundreds of coefficients. Typing them from a book is how you get a library that's subtly wrong forever. Instead, a script downloads the official VSOP87D data file from the CDS astronomical data center, truncates it by amplitude, and emits the TypeScript table with provenance in the header. The Moon's 120-term table I did have to type from Meeus, but the fixtures had my back: the mean error came out at 10.5″, exactly the intrinsic accuracy Meeus states for that truncation. A single typo would have inflated it.

The bugs the fixtures caught

The nutation fingerprint. My lunar-node function disagreed with the reference by a systematic 11 to 18 arcseconds. The scale was the clue: nutation oscillates ±17.2″. Swiss Ephemeris reports the node on the true equinox of date; my polynomial gave the mean equinox. One line fixed it, and max error fell to 0.35″. Good reference data doesn't just say wrong. The error's shape tells you why.

Sunset before sunrise. At 71°N, near the midnight-sun transition, a local calendar day can contain a sunset at 01:30 and a sunrise at 02:40, in that order. My test asserted sunrise < sunset as a universal property. The fixture data taught me otherwise.

The neighbouring solar cycle. Also at 71°N: a local day whose sunset belongs to the previous day's solar arc, 15 minutes after local midnight. My first algorithm only searched around that day's own solar noon and reported "always up." The fix: consider events from adjacent transits and keep whatever lands inside the local day.

The refraction calibration. First full run: every site showed sunrise late and sunset early by the same amount. The day was symmetrically too short, and that signature means exactly one thing: the horizon-altitude constant is wrong. The textbook says 34′ of refraction; Swiss Ephemeris effectively bends about 36.7′ at the event's true altitude. At tangent polar crossings that 0.6′ costs two minutes. One calibrated constant, documented with its provenance, took max error from 115 s to 4.6 s.

The empty tarball. My npm build hook was prepublishOnly, which does not run on pack. CI packed tarballs with no compiled code in them. I only caught it because the CI installs the packed tarballs with plain npm into a scratch project on Node 18/20/22 and runs a smoke test, exactly as a consumer would. If your CI only ever tests your source tree, you are not testing the thing you ship.

Architecture: secular core, cultural layers

@grahan/core is pure astronomy. A weather or photography app can use it without touching a single Vedic concept. @grahan/vedic layers the cultural interpretation on top: the Lahiri ayanamsa (fitted to reference values to 0.002″), then tithi, nakshatra, yoga, and karana as pure functions over longitudes. Planned layers on the same core: world calendars (Bikram Sambat, Hijri, Hebrew), prayer times, tropical charts.

A detail I enjoyed: the ayanamsa cancels in the Moon minus Sun difference, so tithi works on tropical longitudes directly. But it doesn't cancel in the Moon plus Sun sum, so yoga genuinely needs the sidereal frame. The kind of thing you only internalize by implementing it.

What's next

Phase 2 is kundali(), full birth charts: the remaining planets, lagna, whole-sign houses, D9, and SVG chart renderers. Same method: fixtures first, formulas second.

Repo: https://github.com/svarbhanu/grahan · Packages: @grahan/core, @grahan/vedic. Issues and PRs welcome, especially from people who know panchang traditions better than I do.

Top comments (0)