This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Go library for astronomical calculations: twilight, lunar phase, rise/set times.
Onboarding references: THEORY.md (Naur-style theory of the codebase) and WALKTHROUGH.md (linear code tour).
Both are hand-maintained prose, brought current at release time in one pass, not as work proceeds —
extend THEORY.md where a load-bearing idea has changed, and regenerate WALKTHROUGH.md with the
code-walkthrough skill. Its snippets are quoted from the source by file and symbol, not by line
range, and nothing in the gate checks them against the files they came from: re-read it before tagging.
Run task --list for the current set.
CI runs exactly task. Never add a check to CI that the local gate does not run,
and never add a tool to the gate without also installing it in the workflow. The Go
toolchain, golangci-lint and prettier are deliberately unpinned: a red gate on untouched
code is the signal working — answer the finding rather than pinning the tool.
Coverage is held by a ratchet, not a percentage: coverage.ratchet records the
count of uncovered statements per package, and task ratchet diffs the current counts
against it — so the gate fails in both directions and on a package appearing or
vanishing. A number that rises is lost coverage; one that falls is coverage to lock in
with task ratchet:update.
An integer rather than a percentage because a percentage holds still while a guarded
branch adds one covered statement and one uncovered, and it grows more forgiving as the
repository grows. The whole check is an awk program in Taskfile.yml; there is no tool
to maintain.
Reflowing blank lines splits coverage blocks, so a refactor can move these counts without changing what the tests reach — read the diff before assuming a regression.
Library is a single package at the repo root, with a reference CLI under cmd/dusk. Zero external
dependencies — nothing outside the standard library, and the Meeus coefficient tables are transcribed
into the source rather than fetched. Module path: github.com/philoserf/dusk/v5.
| File | Domain |
|---|---|
dusk.go |
Package doc, Observer/NewObserver, event types, stringError, all sentinels but one |
solar.go |
SunriseSunset, civil/nautical/astronomical twilight, all unexported solar helpers |
lunar.go |
MoonriseMoonset, LunarPhase, unexported lunar helpers, Meeus Table 47.A/B coefficients |
epoch.go |
Julian dates, sidereal time, nutation, obliquity — unexported apart from ErrDateOutOfRange, which lives beside the range check that returns it |
coord.go |
Coordinate conversions: eclipticToEquatorial, altitudeOf, hourAngle. Sits one layer above epoch.go; reached only by MoonriseMoonset's scan |
trig.go |
Degree-based trig wrappers, clamp, mod360/mod24 normalization |
cmd/dusk/ |
Reference CLI over the public API: main.go (flags, errors), report.go (assembly), render.go (text/JSON) |
cmd/dusk is an executable specification, not a product: it calls every exported function, and every
documented edge case is reachable with a single flag. Its exit contract is part of that — polar geometry
is a result, so the report renders and exits 0; only a misused command line (usage:) and an
out-of-range date (unsupported date:) exit 1, told apart by the message rather than the status.
- All angles in degrees (trig helpers in
trig.gohandle conversion) - Angle normalization via
mod360()andmod24()helpers - Longitude is east-positive, west-negative (New York is -74.006)
- Meeus algorithms preferred;
solarMeanAnomaly(J)takes days, not centuries Observerconstructed viaNewObserver— validates once at creation, fields unexported- The calendar day is a type, not a convention.
SunriseSunset,TwilightandMoonriseMoonsettake aDate{Year, Month, Day};DateIn(t, loc)converts an instant. Atime.Timeday would be read in the observer's zone, where atime.UTCmidnight falls on the previous day west of Greenwich. Internally, solar builds UTC midnight and lunar builds true local midnight LunarPhaseis the exception to the day regime: it takes an instant, not a day, and noObserver— phase is Sun-Earth-Moon geometry, so the observer is irrelevantTwilight(date, obs, depression)returns both boundaries on the queried day — oneHorizongoverns the day, so either both exist or neither does. Overnight darkness is two calls at the call site- The text report rounds to the minute; the JSON does not.
cmd/dusk's timeline rounds where an event enters it, not at theFormatcall — the day-marker logic compares against the report's day, so a 23:59:45 sunset has to round to tomorrow's 00:00 before that comparison. JSON keeps its seconds as the machine-readable answer, so the two renderings of one event may differ by up to half a minute, deliberately - Zero-value
time.Timesignals "event did not occur" — check with.IsZero() - Polar geometry is a value, not an error.
SunEvent.Horizon/TwilightEvent.HorizonareCrosses/StaysAbove/StaysBelow;Rise/Set/Dawn/Duskare zero unlessCrosses.Noonis always set andDurationis 24h or 0. The names describe the geometry because at a depression angle "circumpolar" and "never rises" meant the opposite of the condition -- which is why those two sentinels are gone.MoonEventis untouched:AboveHorizonanswers a different question errorreturns for date out of range (validated at all public entry points)- Sentinel errors are
const, notvar— declared as the unexportedstringErrorstring type indusk.goso they cannot be reassigned. New sentinels follow that pattern, noterrors.New - Table-driven tests everywhere, expected values from USNO/Stellarium/Meeus. Tolerances that reference data actually supports: 2 minutes for sunrise/sunset, 2 minutes for civil twilight, 4 minutes for nautical and astronomical twilight (a 12-18° depression amplifies declination error, so twilight does not inherit sunrise's figure), 1 minute for moonrise/moonset (measured margin is tens of seconds since the parallax-corrected threshold landed), 1-2% for lunar illumination
- A pin is either a reference value or a regression pin, and the comment says which.
USNO's one-day service publishes sunrise/sunset and civil twilight only, so the nautical
pin in
solar_test.gois uncorroborated and labelled as such. Record the measured margin beside a tolerance so the next reader can tell a tight test from a lucky one - Every test and subtest is parallel (
paralleltest), so accumulating state across subtests is a race — check table-wide properties in their own sequential test instead
.golangci.yml runs default: all and disables only what fights this repo's deliberate design.
Each disable carries a measured finding count and a reason — the file's own rule, and the bar for
adding another: count the findings, read them, and write down why they are wrong here.
- Terse Meeus notation (
T,M,Lp,Mp,h0) is permitted by name in thevarnamelenignore list and by disablinggocritic'scaptLocal; a new one-letter name needs an entry - gofumpt and goimports run inside golangci-lint, which is the single definition of formatted
this repo has for Go. Two
PostToolUsehooks in.claude/settings.jsonact on each Go file anEditorWritetouches, reading its path from the hook's stdin JSON: one runsgofumpt -extra -won it, the other runsgo test -count=1 -short ./...and reports the last line. The Brewfile does not install gofumpt, so without it the format hook fails harmlessly (a non-blocking error) andtask lintis what catches the formatting - prettier is the same thing for every file that is not Go — Markdown, JSON and YAML — run
by
task docsand configured by.prettierrc.json.embeddedLanguageFormatting: "off"is the load-bearing setting: prettier's default rewrites source inside fenced blocks, and this repository's documents quote their own compiled examples..prettierignorenames what is out of scope and why
- Moonrise/moonset iterates minute-by-minute (1440 iterations) — slow by design
solarHourAnglereturns(float64, Horizon)— the angle is zero and meaningless unless the Horizon isCrosses- The two solar boundaries are solved independently, not mirrored.
solarCrossingtakes a first symmetric estimate about transit, then re-solves each boundary against the declination at its own instant — near an equinox declination moves ~0.4°/day, so the afternoon half-day really is shorter. Worst measured disagreement with USNO is 115s, from 176s. It recovers about half the true skew; the rest needs the hour angle measured against the Sun's own right ascension rather than a fixed transit, which is #114 solarHourAngletakesdepression(positive degrees below horizon) for twilight reuse; pass 0 for sunrise/sunseteclipticToEquatorialapplies full nutation (Δψ + Δε);solarDeclinationuses mean obliquity only (intentional asymmetry — NOAA simplified method for sunrise/sunset)- The Moon's horizon threshold is positive and the Sun's is negative. Meeus's lunar
h0 = 0.7275·π − 0.5667is about +0.125°, because horizontal parallax outweighs refraction for the one body close enough for it to matter; the Sun's is −0.833°. The scan therefore comparesaltitude > h0, notaltitude > -h0. Using 0.833 for both — which this code did until v4.1.0 — biases every moonrise early and every moonset late by 5–12 minutes.h0is also recomputed per sample from the Moon's true distance, so it is a function, not a constant - A reported moon crossing is interpolated, and lands in
[cur−1m, cur). That half-open interval is an invariant, not an implementation detail: it is what keeps the scan's closed upper bound from filing an event under the next calendar day, and it is whatFuzzMoonriseMoonsetasserts.crossingInstantinterpolates on the differencealtitude − h0, never on altitude against a fixed threshold - An unexported function with a test is invisible to
unused. The gate cannot tell you when the last production caller disappears — this bit twice, withsolarPositionand thenlunarPosition. After deleting or rerouting a call site,grepfor the callee - Nothing in the gate reads prose.
go builddoes not resolve godoc links,go vetdoes not check a comment against the code beneath it, andrelease-gate's walkthrough andCLAUDE.mdrows measure staleness by commit count rather than accuracy. v5.0.0 deleted five exported symbols and left four doc comments describing them; three survived a green gate through every PR in the release, and the fourth was caught only becauselllobjected to the line's length. After deleting or renaming an exported symbol,grepfor its name in comments as well as code, and treat a doc comment next to changed code as part of the change rather than as documentation to revisit later
The /vN in the module path breaks .golangci.yml and coverage.ratchet until both
are hand-edited — see the major-version-bump skill.
No release automation. CHANGELOG.md is written by hand. Tag last, by hand, after the gate
is green and WALKTHROUGH.md has been re-verified against the current source.
One GitHub Actions job that installs the toolchain and runs task. No paths-ignore: a
gate that skips a documentation-only push cannot catch a broken example, and the examples
here compile.