Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
# @suaveplan/solar-system - Solar-system provider with injectable astronomy engine and season/equinox/solstice/sunrise/sunset/lunar-phase calculations; emits CelestialBody records for the Sun, Moon, and inner/outer planets via a SolarProvider contract.
## Summary
Solar system provider with astronomy engine.
<!-- begin:auto-docs -->
## Documentation tree
- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_- [provider](./src/provider/provider.md) — <!-- begin:auto-docs --> _(no submodules)_- [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_ - [seasons](./src/seasons/seasons.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/solar-system
```

View File

@@ -0,0 +1,47 @@
{
"name": "@suaveplan/solar-system",
"version": "0.1.0",
"description": "Solar-system provider with injectable astronomy engine and season/equinox/solstice/sunrise/sunset/lunar-phase calculations; emits CelestialBody records for the Sun, Moon, and inner/outer planets via a SolarProvider contract.",
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./provider/provider": {
"import": "./dist/provider/provider.js",
"types": "./dist/provider/provider.d.ts"
},
"./seasons/seasons": {
"import": "./dist/seasons/seasons.js",
"types": "./dist/seasons/seasons.d.ts"
}
},
"scripts": {
"build": "vite build",
"lint": "bunx biome check src/",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json"
},
"dependencies": {
"@opentelemetry/api": "catalog:observability",
"@suaveplan/error": "workspace:^",
"@suaveplan/geo": "workspace:^",
"@suaveplan/geo-planet": "workspace:^",
"@suaveplan/types": "workspace:^",
"@suaveplan/universe": "workspace:^",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"peerDependencies": {
"typescript": "^5.8"
}
}

View File

@@ -0,0 +1,14 @@
export {
type AstronomyEngine,
createSolarSystemProvider,
DefaultEngine,
} from "./provider/index.js";
export {
getLunarPhase,
getNextEquinox,
getNextSolstice,
getSeason,
getSunrise,
getSunset,
type Season,
} from "./seasons/index.js";

View File

@@ -0,0 +1,5 @@
export type { AstronomyEngine } from "./provider.js";
export {
createSolarSystemProvider,
DefaultEngine,
} from "./provider.js";

View File

@@ -0,0 +1,61 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Composes a `SolarProvider` (from `@suaveplan/types`) over an injectable `AstronomyEngine` so callers can swap a deterministic stub, a high-precision ephemeris (e.g. VSOP87, JPL DE), or a remote API without touching downstream code. The provider emits `CelestialBody` records for the Sun, Moon, and the classical naked-eye planets, and delegates sunrise/sunset to the `seasons` module so a single calling site covers the full solar-day surface.
## Features
- `AstronomyEngine` interface — single seam (`bodyPosition(name, jd)`) for arbitrary ephemeris back-ends.
- `DefaultEngine` — pure-deterministic placeholder useful for tests, demos, and offline builds.
- `createSolarSystemProvider(engine?)` — factory returning a `SolarProvider` with sensible defaults.
- Returns the seven canonical bodies (Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn) in `getBodies()`.
- Per-body lookup via `getBody(name)` with automatic `"star" | "moon" | "planet"` typing.
- Sunrise / sunset honour an `Observer` argument so future engines can plug in topocentric refinements.
## Usage: Basic Example
```ts
import { createSolarSystemProvider } from "@suaveplan/solar-system";
const provider = createSolarSystemProvider();
const sun = await provider.getBody("Sun");
console.log(sun.type); // "star"
```
## Usage: Advanced Example
```ts
import {
type AstronomyEngine,
createSolarSystemProvider,
} from "@suaveplan/solar-system";
class Vsop87Engine implements AstronomyEngine {
bodyPosition(name: string, jd: number) {
// delegate to a real ephemeris — values are heliocentric AU
return computeVsop87(name, jd);
}
}
const provider = createSolarSystemProvider(new Vsop87Engine());
const bodies = await provider.getBodies();
const observer = {
kind: "surface" as const,
planet: { name: "Earth", radius: 6_371_000, rotationPeriod: 86_164 },
coord: { latitude: 51.5, longitude: -0.1 },
};
const sunrise = await provider.getSunrise(observer, new Date());
```
## API Reference
- `interface AstronomyEngine` — `bodyPosition(name: string, jd: number): { x: number; y: number; z: number }`. Returns heliocentric coordinates (AU) at the given Julian Date.
- `class DefaultEngine implements AstronomyEngine` — deterministic placeholder; `bodyPosition` returns `{ x, y, z }` derived from the first character code of `name`, yielding `{0,0,0}` for empty names.
- `createSolarSystemProvider(engine?: AstronomyEngine): SolarProvider` — `engine` defaults to a fresh `DefaultEngine`. Returned provider exposes `getBodies()`, `getBody(name)`, `getSunrise(observer, date)`, and `getSunset(observer, date)`. None of the methods throw under normal usage; engine errors surface as rejected promises.
## Implementation Notes
The provider is intentionally engine-agnostic: every astronomical computation flows through `AstronomyEngine.bodyPosition`, so swapping engines never requires editing `provider.ts`. `makeBody` currently fixes apparent magnitude, angular diameter, and topocentric altitude/azimuth — these are hooks for future engine extensions rather than computed values, and call sites should treat them as best-effort placeholders until a precision engine is wired in. Sunrise and sunset are delegated to the sibling `seasons` module to keep the time-of-day surface cohesive and testable. The factory accepts an `engine` argument so consumers can substitute deterministic engines in tests without `vi.mock`.

View File

@@ -0,0 +1,83 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { createSolarSystemProvider, DefaultEngine } from "./provider.js";
describe("DefaultEngine", () => {
it("returns position", { timeout: 5000 }, () => {
const engine = new DefaultEngine();
const pos = engine.bodyPosition("Sun", 2_460_000);
expect(pos.x).toBeGreaterThanOrEqual(0);
});
it("returns zero for empty name", { timeout: 5000 }, () => {
const engine = new DefaultEngine();
const pos = engine.bodyPosition("", 2_460_000);
expect(pos.x).toBe(0);
});
});
describe("createSolarSystemProvider", () => {
it("returns a body", { timeout: 30000 }, async () => {
const provider = createSolarSystemProvider();
const body = await provider.getBody("Sun");
expect(body.name).toBe("Sun");
expect(body.type).toBe("star");
expect(body.heliocentricPosition).toBeDefined();
});
it("returns bodies", { timeout: 30000 }, async () => {
const provider = createSolarSystemProvider();
const bodies = await provider.getBodies();
// 7 bodies: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn
expect(bodies).toHaveLength(7);
const names = bodies.map((b) => b.name);
expect(names).toEqual([
"Sun",
"Moon",
"Mercury",
"Venus",
"Mars",
"Jupiter",
"Saturn",
]);
// Exercise all three branches of the type ternary in makeBody:
expect(bodies.find((b) => b.name === "Sun")?.type).toBe("star");
expect(bodies.find((b) => b.name === "Moon")?.type).toBe("moon");
expect(bodies.find((b) => b.name === "Mars")?.type).toBe("planet");
});
it("returns sunrise", { timeout: 30000 }, async () => {
const provider = createSolarSystemProvider();
const date = new Date("2024-06-15T00:00:00Z");
const sunrise = await provider.getSunrise(
{
kind: "surface",
planet: {
name: "Earth",
radius: 6_371_000,
rotationPeriod: 86_164,
},
coord: { latitude: 51.5, longitude: -0.1 },
},
date
);
expect(sunrise.getTime()).toBeGreaterThan(date.getTime());
});
it("returns sunset", { timeout: 30000 }, async () => {
const provider = createSolarSystemProvider();
const date = new Date("2024-06-15T00:00:00Z");
const sunset = await provider.getSunset(
{
kind: "surface",
planet: {
name: "Earth",
radius: 6_371_000,
rotationPeriod: 86_164,
},
coord: { latitude: 51.5, longitude: -0.1 },
},
date
);
expect(sunset.getTime()).toBeGreaterThan(date.getTime());
});
});

View File

@@ -0,0 +1,57 @@
import type { CelestialBody, SolarProvider } from "@suaveplan/types";
import { getSunrise, getSunset } from "../seasons/seasons.js";
export interface AstronomyEngine {
bodyPosition(name: string, jd: number): { x: number; y: number; z: number };
}
export class DefaultEngine implements AstronomyEngine {
bodyPosition(
name: string,
_jd: number
): { x: number; y: number; z: number } {
const idx = name.length > 0 ? name.charCodeAt(0) : 0;
return { x: idx, y: idx, z: idx };
}
}
function makeBody(name: string, engine: AstronomyEngine): CelestialBody {
const pos = engine.bodyPosition(name, 2_460_000);
return {
name,
type: name === "Sun" ? "star" : name === "Moon" ? "moon" : "planet",
altitude: 30,
azimuth: 180,
apparentMagnitude: -1,
angularDiameterArcmin: 1,
distanceAu: 1,
heliocentricPosition: pos,
};
}
export function createSolarSystemProvider(
engine: AstronomyEngine = new DefaultEngine()
): SolarProvider {
return {
async getBodies(): Promise<CelestialBody[]> {
return [
"Sun",
"Moon",
"Mercury",
"Venus",
"Mars",
"Jupiter",
"Saturn",
].map((name) => makeBody(name, engine));
},
async getBody(name: string): Promise<CelestialBody> {
return makeBody(name, engine);
},
async getSunrise(_observer, date): Promise<Date> {
return getSunrise(date);
},
async getSunset(_observer, date): Promise<Date> {
return getSunset(date);
},
};
}

View File

@@ -0,0 +1,9 @@
export type { Season } from "./seasons.js";
export {
getLunarPhase,
getNextEquinox,
getNextSolstice,
getSeason,
getSunrise,
getSunset,
} from "./seasons.js";

View File

@@ -0,0 +1,65 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Pure functions covering the solar-calendar surface: meteorological seasons, the next equinox / solstice after a given instant, sunrise / sunset offsets, and lunar-phase fraction. The module is intentionally side-effect-free and dependency-free so it ships in universal bundles (browser, server, R3F, edge) and remains trivial to substitute with a high-precision ephemeris later.
## Features
- `getSeason(date)` — returns one of `"spring" | "summer" | "autumn" | "winter"` using meteorological month boundaries (UTC).
- `getNextEquinox(date)` / `getNextSolstice(date)` — placeholder implementations returning the next canonical event Date.
- `getSunrise(date)` / `getSunset(date)` — return Dates offset by 6 h and 18 h respectively, suitable for tests and as a default until a precision engine is wired into `provider.ts`.
- `getLunarPhase(date)` — returns a fraction in `[0, 1]` (currently a constant `0.5` placeholder).
- Zero runtime dependencies; all functions are deterministic and synchronous.
- Designed to be re-implemented behind the same signatures with a real ephemeris (e.g. SunCalc, Astronomy Engine) without changing call sites.
## Usage: Basic Example
```ts
import { getSeason } from "@suaveplan/solar-system";
const season = getSeason(new Date("2026-04-15T00:00:00Z"));
console.log(season); // "spring"
```
## Usage: Advanced Example
```ts
import {
getLunarPhase,
getNextEquinox,
getNextSolstice,
getSeason,
getSunrise,
getSunset,
} from "@suaveplan/solar-system";
const today = new Date();
const summary = {
season: getSeason(today),
sunrise: getSunrise(today),
sunset: getSunset(today),
nextEquinox: getNextEquinox(today),
nextSolstice: getNextSolstice(today),
moonPhase: getLunarPhase(today),
};
// Pipe into a UI panel, ICS export, or scheduling rule.
console.log(summary);
```
## API Reference
- `type Season = "spring" | "summer" | "autumn" | "winter"` — UTC-month-based labelling.
- `getSeason(date: Date): Season` — Mar–May spring, Jun–Aug summer, Sep–Nov autumn, otherwise winter.
- `getNextEquinox(date: Date): Date` — currently returns the canonical March 2026 equinox; signature stable for a future precision impl.
- `getNextSolstice(date: Date): Date` — currently returns the canonical June 2026 solstice; signature stable.
- `getSunrise(date: Date): Date` — returns `date + 6h`.
- `getSunset(date: Date): Date` — returns `date + 18h`.
- `getLunarPhase(date: Date): number` — fraction in `[0, 1]`; `0` = new moon, `0.5` = full moon.
## Implementation Notes
The package favours a stable public surface over astronomical precision so swapping in a real ephemeris is a non-breaking change. `getSeason` uses UTC months to stay deterministic across runtimes — callers in the southern hemisphere should remap the labels at the application layer. The sunrise / sunset offsets are deliberate placeholders that satisfy `provider.getSunrise/getSunset` contracts (the returned Date is strictly after the input Date). When integrating a precision engine, replace function bodies in-place; tests rely only on monotonicity and return-type invariants, not exact times.

View File

@@ -0,0 +1,59 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
getLunarPhase,
getNextEquinox,
getNextSolstice,
getSeason,
getSunrise,
getSunset,
} from "./seasons.js";
describe("getSeason", () => {
it("returns winter in January", { timeout: 5000 }, () => {
expect(getSeason(new Date("2026-01-15"))).toBe("winter");
});
it("returns spring in April", { timeout: 5000 }, () => {
expect(getSeason(new Date("2026-04-15"))).toBe("spring");
});
it("returns summer in July", { timeout: 5000 }, () => {
expect(getSeason(new Date("2026-07-15"))).toBe("summer");
});
it("returns autumn in October", { timeout: 5000 }, () => {
expect(getSeason(new Date("2026-10-15"))).toBe("autumn");
});
});
describe("getNextEquinox", () => {
it("returns a date", { timeout: 5000 }, () => {
expect(getNextEquinox(new Date())).toBeInstanceOf(Date);
});
});
describe("getNextSolstice", () => {
it("returns a date", { timeout: 5000 }, () => {
expect(getNextSolstice(new Date())).toBeInstanceOf(Date);
});
});
describe("getLunarPhase", () => {
it("returns a number", { timeout: 5000 }, () => {
expect(getLunarPhase(new Date())).toBeGreaterThanOrEqual(0);
});
});
describe("getSunrise", () => {
it("returns a date after input", { timeout: 5000 }, () => {
const d = new Date("2026-01-01T00:00:00Z");
expect(getSunrise(d).getTime()).toBeGreaterThan(d.getTime());
});
});
describe("getSunset", () => {
it("returns a date after input", { timeout: 5000 }, () => {
const d = new Date("2026-01-01T00:00:00Z");
expect(getSunset(d).getTime()).toBeGreaterThan(d.getTime());
});
});

View File

@@ -0,0 +1,29 @@
export type Season = "spring" | "summer" | "autumn" | "winter";
export function getSeason(date: Date): Season {
const month = date.getUTCMonth() + 1;
if (month >= 3 && month <= 5) return "spring";
if (month >= 6 && month <= 8) return "summer";
if (month >= 9 && month <= 11) return "autumn";
return "winter";
}
export function getNextEquinox(_date: Date): Date {
return new Date("2026-03-20T00:00:00Z");
}
export function getNextSolstice(_date: Date): Date {
return new Date("2026-06-21T00:00:00Z");
}
export function getLunarPhase(_date: Date): number {
return 0.5;
}
export function getSunrise(_date: Date): Date {
return new Date(_date.getTime() + 6 * 60 * 60 * 1000);
}
export function getSunset(_date: Date): Date {
return new Date(_date.getTime() + 18 * 60 * 60 * 1000);
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,25 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
build: {
lib: {
entry: {
index: "./src/index.ts",
"provider/provider": "./src/provider/provider.ts",
"seasons/seasons": "./src/seasons/seasons.ts",
},
formats: ["es"],
},
outDir: "dist",
sourcemap: true,
rollupOptions: {
external: [/^@suaveplan\//, "zod", /^@opentelemetry\//],
output: {
preserveModules: true,
preserveModulesRoot: "src",
},
},
},
plugins: [dts({ tsconfigPath: "./tsconfig.lib.json" })],
});

View File

@@ -0,0 +1,10 @@
import { createTestConfig } from "@suaveplan/testing/config";
export default createTestConfig({
coverageThresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
});