skymath 0.2.0

Planning-grade astronomy math for astrophotography tooling: angles, equatorial coordinates, sexagesimal parsing and formatting, angular separation and offsets, precession, MJD/JD/calendar conversions, sidereal time, and observer-local quantities (alt-az, airmass, transit).
Documentation
# Decisions, Assumptions & Questions — skymath v0.1

Running log for the autonomous build. **[DECIDED]** = confirmed by maintainer in the
pre-spec grilling · **[DECISION]** = made autonomously during the build ·
**[ASSUMPTION]** = default taken, flagged for review · **[QUESTION]** = open, needs
maintainer input.

## Maintainer-confirmed (grilling, 2026-07-11)

- **[DECIDED] Repo public** under nightwatch-astro.
- **[DECIDED] Hard cut for target-match**: no compatibility re-exports; target-match 0.2
  uses skymath types directly in its signatures ("it's greenfield, just change it").
- **[DECIDED] Single crate**; only optional feature is `serde`; unconditional deps
  `thiserror` + `time` only.
- **[DECIDED] Fully typed public API** — no parallel f64 convenience API; results in
  small typed structs, not tuples.
- **[DECIDED] `OffsetDateTime` for all ephemeris inputs** (misuse of local time made
  structurally impossible); FITS `DATE-OBS` parsing returns `PrimitiveDateTime` with an
  explicit documented UTC bridge.
- **[DECIDED] Lenient sexagesimal parsing errors on garbage tokens, always.** Lenient
  means flexible separators + missing-field defaults, never acceptance of corrupt input.
  (Behavior change vs fits-header/alm metadata-core; flagged in fits-header#6.)
- **[DECIDED] Accuracy contract**: public promise ≤1 arcminute vs AstroPy/ERFA-derived
  references; internal test tolerances stricter where algorithms are exact (rotations
  ~mas, GMST ±0.1 s, airmass/refraction ~1%).
- **[DECIDED] Streamlined SpecKit run**: specify → plan → tasks → implement → verify,
  autonomous, this grilling standing in for the clarify gate, plus a final
  speckit-verify (requirements) pass.
- **[DECIDED] v0.1 scope** = Tier 1 (extraction) + LST/Alt-Az observer tier; sun/moon/
  twilight/Lorentzian-avoidance/constellations staged to v0.2; apparent-place astrometry
  permanently out.

## Build-phase log

- **[DECISION] Feature numbering/branch**: `001-skymath-core`, mirroring the
  target-match convention (spec dir `specs/001-skymath-core`, PR from branch to main).
- **[DECISION] Version bootstrap**: Cargo.toml + release-please manifest start at 0.0.0
  so the first `feat:` release PR cuts v0.1.0 exactly when the implementation lands.
- **[DECISION] speckit-setup script bug worked around**: upstream setup script crashes
  at its summary stage (`FAILED_EXTENSIONS` unbound when zero extensions fail); the two
  remaining documented steps (workflow definitions, spec-status gitignore entry) were
  run directly per the skill's own documentation.
- **[ASSUMPTION] Optional speckit hooks skipped** (tinyspec classify, refine.status,
  worktree.create): feature is clearly not tiny; repo is dedicated so no worktree needed.
- **[ASSUMPTION] APM dependencies unpinned** (mirrors target-match's apm.yml exactly,
  which is also unpinned); pinning is a family-wide hygiene task, not a skymath concern.
- **[DECISION] astro-math donor reality check**: `gaker/astro-math` 0.2.1 (latest
  published) ships no `refraction`, `galactic`, or `rise_set` modules and no 30 KB
  AstroPy vector file — only `transforms`/`sidereal`/`location`/`time`/`projection`.
  The alt-az transform and its AstroPy cross-check vectors were ported as planned;
  airmass (Kasten–Young 1989), refraction (Bennett/Sæmundsson), and the galactic
  rotation were written fresh against published values. research.md carries the
  correction note; NOTICE attribution stands for the ported parts.
- **[DECISION] T010 file-name deviation**: no `tests/coordinates_only.rs` — the donor
  file of that name tests matcher behaviour (target-match scope). The migrated
  coordinate suites live inline in `src/angle.rs`/`src/coords.rs` and in the
  properties/known-values suites.
- **[DECISION] `serde` feature pulls `time/serde-human-readable`** so
  `CrossingOutcome` (which embeds `OffsetDateTime`) serializes as RFC-3339 strings
  in JSON rather than opaque tuples.
- **[DECISION] AstroPy cross-validation via generated pinned vectors** (2026-07-12):
  `scripts/gen_astropy_vectors.py` (run with `uv run --with astropy --with astroplan`)
  bakes AstroPy 8.0.1 / astroplan 0.10.1 outputs into `tests/data/astropy_vectors.json`;
  `tests/astropy_vectors.rs` validates the full public surface against it with no
  Python at test time. Airmass (Kasten–Young) and refraction (Bennett/Sæmundsson)
  have no AstroPy analogue and stay pinned to their published values.
- **[DECISION] 002 ephemerides (2026-07-12)**: Sun (Meeus 25 low-accuracy), Moon
  (truncated ELP-2000/82, Meeus 47 — term tables ported from `saurvs/astro-rust`,
  MIT, NOTICE updated; ch. 40 topocentric; ch. 48 illumination), twilight and
  moonrise/set via a moving-body iteration of the analytic crossing solver, and the
  ACP/Berry moon-avoidance Lorentzian. UTC≈TD accepted (Sun ~3″, Moon ~38″ — inside
  the ≤1′/≤2′ claims; the spec's first ΔT estimate understated the lunar term and
  was corrected in research R18). AstroPy suite extended (`get_sun`/`get_body`
  return GCRS, i.e. J2000-aligned — of-date results precess back for comparison);
  astroplan's 1992 illumination reproduces Meeus 48.a exactly, cross-confirming
  both oracles. Constellation identification stays deferred (003 candidate).
- **[BUG FOUND & FIXED by the AstroPy suite] Observer functions did not precess**:
  hour_angle/alt_az/parallactic/transit/crossings compared J2000 RA against of-date
  sidereal time (~2 s of RA per year, ≈13′ error for J2000 targets in 2026 — outside
  the 1′ contract). All observer entry points now precess the target to the epoch of
  the instant internally. The astro-math donor has the same defect: its "AstroPy
  verified" Kitt Peak vector was actually its own unprecessed output (AstroPy
  disagrees by ~10′); `tests/ported_vectors.rs` now pins genuine AstroPy values and
  NOTICE no longer credits the vectors.