← Projects

How eclipse-sim was built

eclipse-sim is a solar-system orrery that runs on Newton's laws instead of a lookup table. It places the Sun, eight planets, and the Moon from their positions on 1 January 2000, integrates gravity forward one small step at a time, and from that single model predicts real solar and lunar eclipses. Next to every prediction sits a “Δ vs catalog” column showing how far it lands from NASA's published time. The miss margin never hides — a prediction you can't check isn't worth much.

One document in, a project out

The entire input was a one-page brief: six features, numeric tolerances written down in advance (“energy drift < 1e-6 over a century”, “every eclipse within ±6 hours of the catalog”), and ground rules — physics from first principles, no ephemeris API at runtime, NASA data allowed only as test fixtures to score against, tests landing with every feature, one branch and PR per feature, an append-only build log, hard stop conditions. The brief was committed at 23:25 and an agent (Claude Opus 4.8) worked it unattended overnight. By 00:12 — 47 minutes later — the integrator, the 3-D orrery, solar-eclipse detection, and the verification panel were all committed with green tests. The lunar-eclipse and saros stretch goal followed by 00:26.

The receipts are public: the brief (ROADMAP.md), the one-line-per-iteration LOG.md, and the git timestamps. Nineteen logged iterations total, no human edits to the code.

The part worth reading: what the debugging taught

The overnight suite was green — and wrong in four places. All four bugs were found the next day by a human scrubbing the timeline by hand, and all four lived in the same spot: not the numeric core, but the stateful plumbing behind the “next eclipse” card (an async, debounced, cached search). Dragging the slider queued thirty seconds of searches. Scrubbing before 2020 skipped two decades. Scrubbing backward served a stale answer. Playing past an eclipse froze the card — the debounce that fixed the drag bug starved under continuous play.

Why did a hundred green tests miss them? Every test fired single actions, forward in time, at happy-middle dates. One test literally asserted the buggy output — a hand-picked expected value written from the same wrong mental model as the code. Coverage looked fine, because coverage measures whether a line ran, not whether any assertion would catch it lying.

The fixes changed the testing, not just the code. The cache decision was extracted into a pure module and put under property-based tests: 6,000+ random slider walks — forward, backward, jumps — checked against an independent oracle after every move, plus a meta-test that re-introduces the real bug and proves the harness catches it. Mutation testing was wired in to measure what coverage can't: the score on the card logic went from 71% to 94%, and the mutants found two real boundary gaps before any human did. The full story is in LESSONS.md in the repo.

The numbers (all reproducible)

Run npm test and npm run test:e2e to regenerate these. Energy drift over a simulated century: 6.1e-11 against a 1e-6 requirement. Planet positions within 0.15° of JPL at five epochs. All 16 solar eclipses 2020–2030 detected, worst timing error 18 minutes against a ±6-hour tolerance; all 22 lunar eclipses, types exact; zero false positives; 3 of 3 saros series derived and matched. 64 unit tests, 71 browser tests, mutation score 94% on the card logic.

One honest limitation, stated on the page itself: it's a point-mass Newtonian model. It nails eclipse timing but is approximate about place — Earth turns 15° an hour, so a ~18-minute timing error smears the ground point east–west by a few hundred kilometres. The one classification it can't resolve is a knife-edge 2023 hybrid whose annular tips are finer than the model can see. It says so rather than special-casing it to look correct.

Everything above is checkable: the repo holds the brief, the build log, the lessons writeup, and every test. The simulator itself runs right here on this site.