target-match 0.3.3

Given a telescope pointing and field of view, rank which catalogued sky objects fall on the frame. Matches by sky position; holds no catalogue data and does no I/O.
Documentation
# Autonomous Decisions, Ambiguities & Input Needed

Running log kept during the autonomous build of `target-match`. Every non-obvious
decision made without a live confirmation is recorded here, plus ambiguities resolved and
anything that still needs the maintainer's input.

Legend: **[DECISION]** made autonomously · **[AMBIGUITY]** resolved with rationale ·
**[INPUT-NEEDED]** awaiting maintainer.

## Project setup & process

- **[DECISION] Public GitHub repo created now.** `github.com/nightwatch-astro/target-match`
  was created **public** (your earlier explicit choice) and the scaffold pushed, so I can
  push throughout. The sibling `fits-header` is private; if you'd rather this be private too,
  it flips trivially: `gh repo edit nightwatch-astro/target-match --visibility private`.
- **[DECISION] SpecKit ceremony streamlined for a solo autonomous run.** The repo's
  `50-speckit-workflow.md` describes a ~19-step flow (clarify/checklist/analyze gates,
  taskstoissues, agent-assign orchestration, multiple review/QA/security subagents). Under
  your "take to completion" mandate I run the generative + verification core
  (specify → plan → tasks → implement → test/verify) and treat the interactive clarify gate
  as satisfied by our extensive grilling. Streamlined steps are noted in the handover.
- **[DECISION] Local directory stays `astro-target-id`.** The crate and repo are
  `target-match`; the working folder name is cosmetic and left as-is to avoid churn.
- **[DECISION] License Apache-2.0 (single).** Your choice; matches `fits-header`. Revisit to
  dual `MIT OR Apache-2.0` before a crates.io publish if broader compatibility is wanted.

## Design defaults taken during grilling (recorded for the record)

- **[DECISION] Offset reporting.** Sky-tangent Δ in **degrees** is always present; frame-aligned
  x/y and **pixel** offsets are additional and only populated when frame orientation and plate
  scale are known.
- **[DECISION] `nearest-N`** = top-N by separation, optionally bounded by a max radius.
- **[DECISION] `is-framed`** = one object + a frame → membership + full geometry.
- **[DECISION] Position angle convention** = degrees **East of North** (standard astronomical).
- **[DECISION] Precession is hand-rolled** (IAU 1976/2006 / Meeus rotation-matrix), no external
  astronomy dependency; validated against reference values.
- **[DECISION] `serde` optional, off by default** even though runtime deps are now allowed —
  forcing serde on every consumer is the one thing library authors still avoid. One flag flips
  it to default-on if you prefer.
- **[DECISION] `anyhow` is dev/examples-only**, never in the public API (it would erase the
  error type callers match on). Public fallible APIs return `target_match::Error` (`thiserror`).

## Ambiguities resolved

- **[AMBIGUITY] "JNow"** interpreted as **mean equinox of date** (precession only). Apparent
  place (nutation/aberration/proper motion) is explicitly excluded — planning-grade doesn't
  need it and it would break the pure/light identity.
- **[AMBIGUITY] Catalogue object epoch.** Objects supplied via `SkyObject` are assumed **J2000**
  (SIMBAD / most FITS convention). If a consumer holds non-J2000 catalogue coordinates, they
  normalize before supplying them (or a future trait extension can carry an epoch).

## Implementation-phase decisions

- **[DECISION] Matching & precession are infallible; two error variants dropped.** Because
  `Epoch::OfDate` always carries its Julian year, "JNow without a date" is unrepresentable.
  So `precess`, `rank`, `Matcher::query`, and `is_framed` return values (not `Result`), and
  the `Error` enum is just `ParseCoord` / `OutOfRange` / `InvalidOptics` — the spec's
  `MissingObservationDate` / `EpochMismatch` are removed as unreachable. This strengthens the
  spec's intent (no silent epoch mis-match) via the type system rather than a runtime check.
  A **minor, strictly-safer deviation** from the literal spec wording (FR-M11); flagged here
  and in the implementation commit. The spec artifacts still describe the original wording;
  the contract doc will be reconciled in the polish pass.
- **[DECISION] `is_framed` is a free function** (plus a thin `Matcher` method) rather than a
  `Query::IsFramed` variant — a single-object query needs no ranking, so a dedicated entry
  point is cleaner than threading one object through `rank`.
- **[DECISION] `Constraint` carries `pixel_scale: Option<(f64,f64)>`.** Pixel offsets need the
  plate scale, which lives on `Field`, not on the raw `Membership` fov. The `within`/`frame`/
  `frame_rotated` builders capture it from the `Field`; manual construction leaves it `None`
  (so pixel offsets are simply absent), matching FR-M7 ("when plate scale is known").
- **[DECISION] Robust tangent-offset geometry.** Offsets/membership use a polar decomposition
  of great-circle separation + position angle into East/North (never dividing by cos of the
  separation), with the circumscribed circle pre-filtering rectangle tests. Exact to planning
  grade for the small fields involved; robust for far objects returned by unbounded nearest-N.
- **[DECISION] Exact `ARCSEC_PER_RADIAN`** (206264.806…) replaces alm's rounded `206.265`.
- **[AMBIGUITY] Binning/FOV model.** `pixels` = the (binned) image dimensions you supply,
  `pixel_um` = physical unbinned pixel size, `binning` = factor. Effective pixel = pixel×binning,
  so ×2 binning ⇒ ×2 scale ⇒ ×2 field for a fixed pixel count (matches SC-009). Halve the pixel
  count for a fixed sensor to keep the field constant.

## Input needed from you

- _(none — all prior items ratified below; further items will be added here if they arise)_

## Ratified 2026-07-11

All open decisions from the v0.1 handover were reviewed and confirmed by the maintainer:

- **Repo visibility** — stays **public**.
- **Infallible matching API** — kept. `MissingObservationDate` / `EpochMismatch` stay dropped;
  the deviation from FR-M11's literal wording is now **ratified**, not merely flagged.
  Tradeoffs reviewed: compile-time unrepresentability beats the runtime check the spec's
  wording asked for; the sole cost is a breaking signature change *if* matching ever becomes
  fallible, acceptable at v0.x. `spec.md` wording intentionally left as the historical record.
- **License** — stays **Apache-2.0 only**; no dual MIT before a crates.io publish.
- **Constitution** — left unratified; principles remain in `AGENTS.md`.

## 0.2 — adopt skymath (2026-07-12)

- **[DECIDED earlier, executed now] Hard cut to `skymath`** (ratified during the skymath
  grilling): the `angle` module (Angle/Epoch/Equatorial, sexagesimal, separation,
  precession) is deleted; `skymath` types appear directly in public signatures. No
  compatibility re-exports of individual types; the `skymath` crate itself is re-exported
  (`target_match::skymath`) for version alignment.
- **[DECISION] API deltas for consumers**: `Equatorial::parse_j2000(ra, dec)`  `skymath::Equatorial::parse_j2000(ra, dec, ParseMode)` (strict/lenient is now explicit);
  `Equatorial::new(ra, dec, epoch)``at_epoch`; sexagesimal formatting via
  `SexaStyle`. `target_match::Error` shrinks to `InvalidOptics` — parse/domain errors
  surface as `skymath::Error` at construction.
- **[DECISION] Test ownership follows code ownership**: the separation/sexagesimal/
  precession property tests moved to skymath with the code; this crate keeps
  matcher/optics coverage (`matcher_equals_rank`, membership geometry, known values).
- **[RESOLVED] Interim git dependency**: skymath v0.1.0 published to crates.io
  (2026-07-12); the pin is now `skymath = "0.1"`. (skymath's first publish was local
  with a token — crates.io Trusted Publishing config can only be added to an existing
  crate; add the config in both crates' settings so future releases publish via OIDC.)