Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
## ADDED Requirements
### Requirement: Earth constant
The system SHALL export a pre-built `EARTH` `PlanetConfig` using the WGS84 ellipsoid, Moon as a `SatelliteConfig`, and ISA atmosphere as `AtmosphereConfig`.
#### Scenario: Earth constant validation
- **WHEN** `EARTH` is imported
- **THEN** `EARTH.name` equals `"Earth"`
- **AND** `EARTH.radius` equals `6378137`
- **AND** `EARTH.satellites` contains exactly one entry with `name` matching `"Moon"`
### Requirement: Earth surface instance
The system SHALL export a pre-built `earthSurface` `PlanetSurface` constructed from `EARTH`.
#### Scenario: Earth surface delegation
- **WHEN** `earthSurface.distance(a, b)` is called for two coordinates
- **THEN** the result equals the WGS84 geodesic distance
### Requirement: Geocoding provider interface
The system SHALL export `GeocodingProvider`, `GeocodingResult`, `createInMemoryProvider`, and `batchGeocode`.
#### Scenario: In-memory geocode
- **WHEN** `createInMemoryProvider([{ name: "London", latitude: 51.5074, longitude: -0.1278 }])` is created
- **AND** `batchGeocode` is called with `["London"]`
- **THEN** the result contains one entry with matching coordinates
### Requirement: Geohash encoding and decoding
The system SHALL export `encodeGeohash` and `decodeGeohash` with round-trip fidelity.
#### Scenario: Geohash round-trip
- **WHEN** `encodeGeohash({ latitude: 51.5074, longitude: -0.1278 }, 9)` is called
- **AND** the result is passed to `decodeGeohash`
- **THEN** the decoded coordinate matches the original within `0.0001` degrees
### Requirement: Coordinate parsing and formatting
The system SHALL export `parseCoordinate` and `formatCoordinate` supporting DMS (degrees-minutes-seconds) and decimal formats.
#### Scenario: Parse DMS string
- **WHEN** `parseCoordinate("51°30'26.5\"N")` is called
- **THEN** the result is approximately `51.5074`
### Requirement: Preserve existing geo public API
The system SHALL preserve all public exports that existed in the legacy `@suaveplan/geo` package.
#### Scenario: Legacy export compatibility
- **WHEN** all public exports of the legacy `@suaveplan/geo` are compared to `@suaveplan/geo-earth`
- **THEN** every legacy export name exists in `geo-earth` with the same or broader type signature

View File

@@ -0,0 +1,67 @@
## ADDED Requirements
### Requirement: PlanetConfig shape
The system SHALL export a `PlanetConfig` type containing name, mass, radius, polar radius, surface gravity, rotation period, axial tilt, optional atmosphere, satellites, optional orbital elements, and optional texture manifest.
#### Scenario: Validate PlanetConfig schema
- **WHEN** a `PlanetConfig` with all required fields is constructed via Zod parse
- **THEN** validation succeeds
- **AND** when mass is omitted, validation throws `ValidationError`
### Requirement: AtmosphereConfig shape
The system SHALL export an `AtmosphereConfig` type with surface pressure, scale height, Rayleigh coefficient, Mie coefficient, and extinction per air mass.
#### Scenario: Validate atmosphere
- **WHEN** an `AtmosphereConfig` with valid positive values is parsed
- **THEN** validation succeeds
- **AND** `atmosphericDensity(0)` on a `PlanetSurface` using this config returns surface pressure-derived density
### Requirement: SatelliteConfig shape
The system SHALL export a `SatelliteConfig` type with name, mass, radius, orbital radius, orbital period, inclination, eccentricity, and albedo.
#### Scenario: Satellite definition
- **WHEN** a `SatelliteConfig` for a moon is parsed with `mass > 0` and `orbitalRadius > 0`
- **THEN** validation succeeds
### Requirement: PlanetSurface geometry delegation
The system SHALL implement `PlanetSurface` whose `distance`, `bearing`, `destination`, and `pointInPolygon` methods delegate to `@suaveplan/geo` functions using the planet's ellipsoid.
#### Scenario: PlanetSurface uses correct ellipsoid
- **WHEN** a `PlanetSurface` is constructed with a planet having `radius = 3396200` and `polarRadius = 3376200` (Mars-like)
- **THEN** `distance` between two surface points matches the great-circle distance on that ellipsoid, not WGS84
### Requirement: Atmospheric density
The system SHALL compute atmospheric density at a given altitude from `AtmosphereConfig` using the scale-height exponential model.
#### Scenario: Density halves at scale height
- **WHEN** `atmosphericDensity(scaleHeight)` is called on a planet with `scaleHeight = 8500`
- **THEN** the result equals half the surface density
### Requirement: Air mass
The system SHALL compute air mass for a given altitude angle using the Kasten–Young formula.
#### Scenario: Zenith air mass
- **WHEN** `airMass(90)` is called
- **THEN** the result is approximately `1.0`
- **AND** `airMass(0)` returns a value significantly greater than `1.0`
### Requirement: No bundled planet data
The system SHALL NOT bundle any pre-defined planet configuration.
#### Scenario: Inspect geo-planet exports
- **WHEN** all exports from `@suaveplan/geo-planet` are enumerated
- **THEN** no `EARTH`, `MARS`, or pre-built `PlanetConfig` constant exists

View File

@@ -0,0 +1,86 @@
## ADDED Requirements
### Requirement: Ellipsoid definition
The system SHALL provide a way to define an ellipsoid from semi-major and semi-minor axes, deriving flattening and eccentricity automatically.
#### Scenario: Define WGS84 ellipsoid
- **WHEN** `defineEllipsoid(6378137, 6356752.314245)` is called
- **THEN** the returned `Ellipsoid` has `semiMajorAxis = 6378137`, `semiMinorAxis = 6356752.314245`, and derived `flattening` and `eccentricity` within floating-point tolerance
### Requirement: Spherical coordinate conversion
The system SHALL convert between spherical coordinates (latitude, longitude, altitude) and Cartesian vectors.
#### Scenario: Convert spherical to Cartesian and back
- **WHEN** a `SphericalCoord` with `latitude = 51.5074`, `longitude = -0.1278`, `altitude = 0` is converted to Cartesian via `sphericalToCartesian` using a WGS84 ellipsoid
- **THEN** converting the result back via `cartesianToSpherical` yields the original coordinates within `0.0001` degrees
### Requirement: Great-circle distance
The system SHALL compute great-circle distance between two spherical coordinates on a given ellipsoid.
#### Scenario: Distance between London and Paris
- **WHEN** `greatCircleDistance` is called with London (`51.5074, -0.1278`) and Paris (`48.8566, 2.3522`) on WGS84
- **THEN** the result is approximately `344000` metres
### Requirement: Vincenty distance
The system SHALL compute geodesic distance using Vincenty's formulae for high precision.
#### Scenario: Vincenty vs great-circle consistency
- **WHEN** both `greatCircleDistance` and `vincentyDistance` are called for the same nearby points (< 100 km)
- **THEN** the results differ by less than `0.1%`
### Requirement: Bearing and destination
The system SHALL compute initial bearing between two points and compute a destination point given start, bearing, and distance.
#### Scenario: Round-trip destination
- **WHEN** `destination(start, bearing, distance, ellipsoid)` is called
- **AND** `bearing` is computed from `start` to a known `end`
- **THEN** the computed destination equals `end` within `0.0001` degrees
### Requirement: Bounding boxes
The system SHALL provide bounding box creation, point containment, expansion, merging, and intersection.
#### Scenario: Box contains point
- **WHEN** a `BoundingBox` spanning `(-10, -10)` to `(10, 10)` is created
- **THEN** `containsPoint(box, { latitude: 0, longitude: 0 })` returns `true`
- **AND** `containsPoint(box, { latitude: 20, longitude: 0 })` returns `false`
### Requirement: Polygon operations
The system SHALL provide point-in-polygon, polygon area, perimeter, and simplification.
#### Scenario: Point inside a square polygon
- **WHEN** a square polygon with vertices at `(0,0)`, `(0,10)`, `(10,10)`, `(10,0)` is defined
- **THEN** `pointInPolygon({ latitude: 5, longitude: 5 }, polygon)` returns `true`
- **AND** `pointInPolygon({ latitude: 15, longitude: 5 }, polygon)` returns `false`
### Requirement: Distance unit conversion
The system SHALL convert distances between metres, kilometres, miles, and nautical miles.
#### Scenario: Convert metres to kilometres
- **WHEN** `convertDistance(1000, 'metre', 'kilometre')` is called
- **THEN** the result equals `1`
### Requirement: No planet-specific constants
The system SHALL NOT export any numeric constant for a specific celestial body.
#### Scenario: Inspect geo exports
- **WHEN** all exports from `@suaveplan/geo` are enumerated
- **THEN** no export named `EARTH`, `MARS`, `MOON`, or similar exists
- **AND** no hardcoded radius, mass, or orbital value for any body is present

View File

@@ -0,0 +1,110 @@
## ADDED Requirements
### Requirement: SolarProvider implementation
The system SHALL export `createSolarSystemProvider` returning a `SolarProvider` that computes real solar system body positions.
#### Scenario: Sun altitude at noon
- **WHEN** `getBody("Sun", observer, time)` is called for an observer at latitude `0` at local noon
- **THEN** the returned `altitude` is approximately `90` minus the absolute declination offset
### Requirement: Astronomy.js engine injection
The system SHALL accept an injectable `AstronomyEngine` in `createSolarSystemProvider`, defaulting to astronomy.js.
#### Scenario: Deterministic engine injection
- **WHEN** a deterministic mock `AstronomyEngine` is injected
- **THEN** `getBodies` returns exactly the mocked positions
### Requirement: Keplerian propagator for custom planets
The system SHALL propagate positions for planets with `OrbitalConfig` using a Keplerian solver from epoch.
#### Scenario: Custom planet position
- **WHEN** a `PlanetConfig` with valid `OrbitalConfig` is used as observer's planet or target body
- **THEN** `getBodies` includes the custom planet with a propagated heliocentric position
### Requirement: Angular diameter computation
The system SHALL compute `angularDiameterArcmin` as `2 * arctan(radius / distance) * (180/π * 60)`.
#### Scenario: Sun angular diameter
- **WHEN** `getBody("Sun", ...)` is called
- **THEN** `angularDiameterArcmin` is approximately `32` arcminutes
### Requirement: Apparent magnitude for real bodies
The system SHALL compute `apparentMagnitude` for real bodies using phase angle and distance via astronomy.js.
#### Scenario: Venus magnitude range
- **WHEN** `getBody("Venus", ...)` is called at multiple times
- **THEN** `apparentMagnitude` varies between approximately `-4.9` and `-3.8`
### Requirement: Sunrise and sunset computation
The system SHALL compute sunrise and sunset dates in UTC for a given observer and calendar date.
#### Scenario: Equator sunrise consistency
- **WHEN** `getSunrise` and `getSunset` are called for an equatorial observer on an equinox date
- **THEN** sunrise is approximately 12 hours before sunset
### Requirement: Season computation
The system SHALL export `getSeason` defaulting to Earth but accepting any `PlanetConfig` with `axialTilt` and `OrbitalConfig`.
#### Scenario: Northern hemisphere season
- **WHEN** `getSeason(new Date("2026-06-21"), EARTH)` is called
- **THEN** the result is `summer`
### Requirement: Equinox and solstice prediction
The system SHALL export `getNextEquinox` and `getNextSolstice` for any configured planet.
#### Scenario: Next equinox after a date
- **WHEN** `getNextEquinox(new Date("2026-01-01"), EARTH)` is called
- **THEN** the result is a `Date` in March 2026
### Requirement: Lunar phase
The system SHALL export `getLunarPhase` returning phase (0–1), illumination percentage, and name for Earth/Moon.
#### Scenario: Full moon phase
- **WHEN** `getLunarPhase(fullMoonDate)` is called
- **THEN** `phase` is approximately `0.5`
- **AND** `illumination` is approximately `100%`
### Requirement: Eclipse prediction
The system SHALL export `predictEclipse` for solar and lunar eclipses on Earth.
#### Scenario: Known eclipse
- **WHEN** `predictEclipse(new Date("2026-01-01"), "solar")` is called
- **THEN** the returned `EclipseEvent` has a date after the input and a `totality` value
### Requirement: Constellation lookup
The system SHALL export `getConstellation` mapping RA/Dec to IAU constellation name.
#### Scenario: Orion lookup
- **WHEN** `getConstellation(raOrion, decOrion)` is called with Orion's centre coordinates
- **THEN** the result equals `"Orion"`
### Requirement: Atmosphere extinction for surface observers
The system SHALL apply atmospheric extinction to apparent magnitudes when the observer has an atmosphere.
#### Scenario: Horizon extinction
- **WHEN** `getBody("Sun", surfaceObserver, time)` is called with the Sun near the horizon
- **THEN** `apparentMagnitude` is dimmer than the vacuum value

View File

@@ -0,0 +1,97 @@
## ADDED Requirements
### Requirement: RawStar shape
The system SHALL export a `RawStar` interface with catalog ID, RA, Dec, apparent magnitude, spectral type, optional distance, proper motion, and parallax.
#### Scenario: Raw star validation
- **WHEN** a `RawStar` with all required fields is parsed
- **THEN** validation succeeds
- **AND** omitting `catalogId` causes validation to fail
### Requirement: StarCatalogSource interface
The system SHALL export a `StarCatalogSource` interface with `id`, `magnitudeLimit`, `starCount`, `queryCone`, and `getById`.
#### Scenario: Source query
- **WHEN** `queryCone` is called on a source with RA, Dec, radius, and options
- **THEN** the returned `RawStar[]` contains only stars within the cone
### Requirement: BSC5 bundled catalog
The system SHALL export `createBsc5Source` returning a `StarCatalogSource` with ~9,096 stars, requiring no network or configuration.
#### Scenario: BSC5 offline query
- **WHEN** `createBsc5Source()` is created and `queryCone(0, 0, 1)` is called
- **THEN** the promise resolves with an array of `RawStar` entries
- **AND** the source `id` equals `"bsc5"`
### Requirement: StarProvider implementation
The system SHALL export `StarProviderImpl` implementing `StarProvider` with priority-ordered catalog sources.
#### Scenario: Visible stars query
- **WHEN** `getVisibleStars` is called with a surface observer and default options
- **THEN** the result is an `ObservedStar[]` sorted by brightness (brightest first)
- **AND** every entry has `altitude` and `azimuth` populated
### Requirement: Horizon frame computation
The system SHALL compute the local horizon frame from `ObserverPosition` for star altitude/azimuth calculation.
#### Scenario: Zenith star altitude
- **WHEN** a star with RA/Dec matching the observer's zenith is queried
- **THEN** the returned `altitude` is approximately `90`
- **AND** `azimuth` is `0` or `180` depending on convention
### Requirement: Atmospheric extinction
The system SHALL apply Pogson's extinction formula using the Kasten–Young air mass when the observer has an `AtmosphereConfig`.
#### Scenario: Extinction at low altitude
- **WHEN** `getVisibleStars` is called for a surface observer on a planet with atmosphere
- **THEN** stars near the horizon have `extinctedMagnitude > apparentMagnitude`
- **AND** stars at the zenith have `extinctedMagnitude ≈ apparentMagnitude`
### Requirement: No extinction in vacuum
The system SHALL NOT apply extinction for `SpacePosition` observers or planets without `AtmosphereConfig`.
#### Scenario: Space observer magnitudes
- **WHEN** `getVisibleStars` is called for a `SpacePosition` observer
- **THEN** `extinctedMagnitude` equals `apparentMagnitude` for all stars
### Requirement: Cone query returns catalog stars
The system SHALL return `CatalogStar[]` (without altitude/azimuth) from `queryCone`.
#### Scenario: Catalog cone query
- **WHEN** `queryCone(ra, dec, radiusDeg)` is called
- **THEN** the result contains `CatalogStar` entries without `altitude` or `azimuth`
### Requirement: SurrealDB adapter interface
The system SHALL export `SurrealAdapterLike`, `SurrealStarProviderOptions`, and `createSurrealStarSource` as a structural interface with no hard dependency on `@suaveplan/surrealdb`.
#### Scenario: Structural satisfaction
- **WHEN** an object with a `query` method matching `SurrealAdapterLike` is passed to `createSurrealStarSource`
- **THEN** the returned object satisfies `StarCatalogSource`
### Requirement: Deferred catalog interfaces
The system SHALL export type signatures for `createHipparcosSource`, `createTycho2Source`, and `createGaiaSource` with injectable `fetchFn` and optional `cacheFn`.
#### Scenario: Interface existence
- **WHEN** the public API surface of `@suaveplan/stars` is inspected
- **THEN** the three deferred factory function types are present
- **AND** their option types include `fetchFn` and `cacheFn` fields

View File

@@ -0,0 +1,114 @@
## ADDED Requirements
### Requirement: TidalProvider implementation
The system SHALL provide `TidalProvider` implementations for Earth (TICON, ADMIRALTY) and any planet (physics-based).
#### Scenario: Provider creation
- **WHEN** `createTiconProvider()`, `createAdmiraltyProvider(options)`, and `createPhysicsBasedProvider()` are called
- **THEN** each returns an object satisfying `TidalProvider`
### Requirement: Earth harmonic providers preserved
The system SHALL preserve TICON and ADMIRALTY provider logic, harmonic constituent engine, and station data.
#### Scenario: TICON prediction
- **WHEN** `predictHeight` is called via a TICON provider for a known UK station at a known time
- **THEN** the result matches the legacy `@suaveplan/tides` value within `0.01` metres
### Requirement: Multi-satellite physics model
The system SHALL compute tidal height from all satellites in `PlanetConfig` using superposition.
#### Scenario: Two-satellite planet
- **WHEN** a `PlanetConfig` with two satellites is used with `createPhysicsBasedProvider()`
- **AND** `predictHeight` is called
- **THEN** the result reflects the combined gravitational influence
### Requirement: Individual satellite contribution
The system SHALL export `satelliteTidalContribution` for visualising per-satellite effects.
#### Scenario: Single contribution
- **WHEN** `satelliteTidalContribution(planet, satellite, coord, time)` is called
- **THEN** the result is a number representing that satellite's height contribution
### Requirement: Zod schemas for all public options
The system SHALL validate all public option types with Zod and throw `ValidationError` on failure.
#### Scenario: Invalid options
- **WHEN** `createAdmiraltyProvider({ apiKey: "" })` is called with an invalid key
- **THEN** validation throws before any network request
### Requirement: AbortSignal on async methods
The system SHALL accept `AbortSignal` on every async provider method.
#### Scenario: Abort tide prediction
- **WHEN** `predictHeight` is called with an aborted signal
- **THEN** the promise rejects with an abort error
### Requirement: TidesError with stable codes
The system SHALL export `TidesError` extending `@suaveplan/error` with `TIDES.*` error codes.
#### Scenario: Error code stability
- **WHEN** a provider throws for a missing station
- **THEN** the error code begins with `TIDES.`
- **AND** the same condition yields the same code across calls
### Requirement: OTel instrumentation
The system SHALL create OTel spans, counters, and histograms on every provider call.
#### Scenario: Span creation
- **WHEN** `predictHeight` is called
- **THEN** an OTel span is started with the operation name
- **AND** a histogram records the prediction latency
### Requirement: Injectable fetch and clock
The system SHALL accept `fetchFn`, `clockFn`, and optional `cacheFn` in ADMIRALTY provider options.
#### Scenario: Custom fetch
- **WHEN** a custom `fetchFn` is injected into `createAdmiraltyProvider`
- **THEN** the provider uses it for all HTTP requests
### Requirement: Property-based tests on harmonic engine
The system SHALL include fast-check property tests on the harmonic prediction engine.
#### Scenario: Property test existence
- **WHEN** the test suite runs
- **THEN** fast-check properties verify monotonicity and bounds of the harmonic sum
### Requirement: TidalProvider routing
The system SHALL export `TidalProviderImpl` routing Earth to `earthProviders` and non-Earth to `physicsProvider`.
#### Scenario: Earth vs Mars routing
- **WHEN** `predictHeight` is called with `EARTH`
- **THEN** the first Earth provider is tried
- **AND** when called with a Mars-like config, the physics provider is used
### Requirement: Planet identification by name
The system SHALL identify Earth by `planet.name.toLowerCase() === "earth"`.
#### Scenario: Name-based routing
- **WHEN** a planet with `name: "Earth"` is passed
- **THEN** it routes to Earth providers
- **AND** a planet with `name: "Mars"` routes to physics

View File

@@ -0,0 +1,115 @@
## ADDED Requirements
### Requirement: ObserverPosition types
The system SHALL export a discriminated union `ObserverPosition` with three variants: `surface` (planet + coord), `atmospheric` (planet + coord + altitude), and `space` (Vec3 + frame).
#### Scenario: Surface observer construction
- **WHEN** an `ObserverPosition` of type `surface` is constructed with a valid `PlanetConfig` and `SphericalCoord`
- **THEN** the object validates successfully
- **AND** the `PlanetConfig` is accessible inline without lookup
### Requirement: CoordinateFrame types and conversion
The system SHALL export `CoordinateFrame` as a union of `heliocentric`, `galactic`, and `custom`, plus pure conversion functions `convertFrame`, `toHeliocentric`, and `toGalactic`.
#### Scenario: Identity conversion
- **WHEN** `convertFrame(vec, heliocentric, heliocentric)` is called
- **THEN** the result equals the input vector
### Requirement: CelestialBody shape
The system SHALL export a `CelestialBody` interface with name, type, altitude, azimuth, apparent magnitude, angular diameter, distance in AU, and heliocentric position.
#### Scenario: Body validation
- **WHEN** a `CelestialBody` object is constructed with all required fields
- **THEN** Zod validation succeeds
### Requirement: CatalogStar and ObservedStar shapes
The system SHALL export `CatalogStar` (catalog-space, no observer fields) and `ObservedStar` (extends `CatalogStar` with altitude, azimuth, and extincted magnitude).
#### Scenario: Observed star construction
- **WHEN** an `ObservedStar` is created from a `CatalogStar` with added `altitude`, `azimuth`, and `extinctedMagnitude`
- **THEN** the object satisfies both the `CatalogStar` and `ObservedStar` contracts
### Requirement: SolarProvider interface
The system SHALL export a `SolarProvider` interface with `getBodies`, `getBody`, `getSunrise`, and `getSunset`, all accepting `ObserverPosition`, `Date`, and optional `AbortSignal`.
#### Scenario: SolarProvider cancellation
- **WHEN** `getBodies` is called with an `AbortSignal` that is aborted before resolution
- **THEN** the promise rejects with an abort error
### Requirement: StarProvider interface
The system SHALL export a `StarProvider` interface with `getVisibleStars` (returns `ObservedStar[]`), `queryCone` (returns `CatalogStar[]`), and `getStar`.
#### Scenario: StarProvider query
- **WHEN** `getVisibleStars` is called with a surface observer and `StarQueryOptions`
- **THEN** the returned array contains only `ObservedStar` entries with `altitude` and `azimuth` populated
### Requirement: TidalProvider interface
The system SHALL export a `TidalProvider` interface with `predictHeight` and `getExtremes`, accepting `PlanetConfig`, `SphericalCoord`, `Date`, and optional `AbortSignal`.
#### Scenario: Tidal prediction cancellation
- **WHEN** `predictHeight` is called with an aborted signal
- **THEN** the promise rejects with an abort error
### Requirement: WeatherState and WeatherProvider interface
The system SHALL export `WeatherState` and `WeatherProvider` with a locked interface for future `@suaveplan/weather` integration.
#### Scenario: WeatherProvider registration
- **WHEN** a `WeatherProvider` is registered with `UniverseClient`
- **THEN** `UniverseScene.weather` is populated after `getScene`
### Requirement: UniverseClient plugin registry
The system SHALL provide `UniverseClient` with typed `register` methods for `solar`, `stars`, `tides`, and `weather` domains, returning `this` for chaining.
#### Scenario: Register all providers
- **WHEN** providers are registered for all four domains
- **AND** `getScene` is called with valid observer and time
- **THEN** the returned `UniverseScene` contains `bodies`, `stars`, `tides`, and `weather`
### Requirement: UniverseScene shape
The system SHALL export `UniverseScene` with `bodies`, `stars`, optional `tides`, and optional `weather`.
#### Scenario: Scene without optional providers
- **WHEN** only `solar` and `stars` providers are registered
- **AND** `getScene` is called
- **THEN** the scene has `bodies` and `stars`, with `tides` and `weather` undefined
### Requirement: StarPacket BitSchema
The system SHALL export a `StarPacket` BitSchema quantising RA, Dec, magnitude, altitude, azimuth, and spectral class into ~11 bytes per star.
#### Scenario: Pack and unpack stars
- **WHEN** an array of `ObservedStar` objects is packed via `packStars`
- **AND** the buffer is unpacked via `unpackStars` with the correct count
- **THEN** the unpacked array has the same length
- **AND** each field is within the quantisation error tolerance of the original
### Requirement: CelestialBodyPacket BitSchema
The system SHALL export a `CelestialBodyPacket` BitSchema quantising altitude, azimuth, angular diameter, apparent magnitude, and body type.
#### Scenario: Pack and unpack bodies
- **WHEN** an array of `CelestialBody` objects is packed via `packBodies`
- **AND** the buffer is unpacked via `unpackBodies` with the correct count
- **THEN** the unpacked array has the same length and matching fields within tolerance