diff --git a/.github/wiki/CODE_OF_CONDUCT.md b/.github/wiki/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4148a1c --- /dev/null +++ b/.github/wiki/CODE_OF_CONDUCT.md @@ -0,0 +1,29 @@ +# Code of Conduct + +## The short version + +Be respectful. Be constructive. Focus on the work, not the person. + +## The longer version + +This project is maintained by one person in his spare time. Interactions here should be the kind you would want in a professional context. + +Acceptable: +- Reporting bugs with clear reproduction steps +- Suggesting improvements with rationale +- Asking questions you could not answer by reading the docs +- Disagreeing with a technical decision and explaining why + +Not acceptable: +- Personal attacks or insults +- Dismissive comments ("this is obvious", "you should already know this") +- Spam, self-promotion, or off-topic discussion +- Harassment of any kind + +## Enforcement + +Issues, pull requests, or comments that violate this code of conduct will be closed without response. Repeat violations result in a block. + +## Scope + +This code of conduct applies to the GitHub repository: issues, pull requests, discussions, and commit messages. diff --git a/.github/wiki/CONTRIBUTING.md b/.github/wiki/CONTRIBUTING.md new file mode 100644 index 0000000..aa56cd8 --- /dev/null +++ b/.github/wiki/CONTRIBUTING.md @@ -0,0 +1,56 @@ +# Contributing to moon-sighting + +Thanks for your interest in contributing. This is a focused library for lunar crescent visibility calculation and contributions are welcome. + +## Getting started + +```bash +git clone https://github.com/acamarata/moon-sighting.git +cd moon-sighting +pnpm install +pnpm build +pnpm test +``` + +All tests should pass before you start. + +## What to work on + +Check the [open issues](https://github.com/acamarata/moon-sighting/issues) for anything tagged `help wanted` or `good first issue`. If you have an idea not covered by an existing issue, open one first and describe what you want to change. That avoids duplicate work. + +## Domain knowledge + +moon-sighting implements the Yallop (1997) and Odeh (2004) crescent visibility criteria and uses the JPL DE442S ephemeris for lunar position calculations. Before contributing to algorithmic code, read the relevant paper: + +- Yallop, B.D. (1997). "A Method for Predicting the First Sighting of the New Crescent Moon." HM Nautical Almanac Office Technical Note No. 69. +- Odeh, M.S. (2004). "New Criterion for Lunar Crescent Visibility." Experimental Astronomy, 18, 39-64. +- Meeus, Jean. "Astronomical Algorithms," 2nd ed. Willmann-Bell, 1998. + +Algorithmic changes require cross-validation against published observation records. See the [Validation](Validation) wiki page for test data sources. + +## Code style + +- TypeScript strict mode. No `any` without a comment explaining why. +- Pure functions, no global state. Observer location is passed explicitly. +- Each function: one purpose. If you can describe it with "and", split it. +- Run `pnpm run format` before committing. CI will fail on formatting issues. +- Run `pnpm run lint` before committing. Fix all warnings, not just errors. + +## Tests + +- Add tests for any new function or changed behavior. +- Tests live in `test.mjs` (ESM) and `test-cjs.cjs` (CommonJS). Both must pass. +- Use the native Node.js `node:test` runner. No Jest, no Vitest. +- Astronomical calculations must be validated against known historical sightings. + +## Pull requests + +- Keep PRs small and focused. One concern per PR. +- Write a clear description of what changed and why. +- For algorithmic changes: include cross-validation data with at least 10 known historical sightings. +- Reference the issue number if one exists (`Fixes #42`). +- CI must be green before merge. This includes test, lint, typecheck, and pack-check. + +## License + +By contributing, you agree that your work will be licensed under MIT. Copyright remains with Aric Camarata. diff --git a/.github/wiki/SECURITY.md b/.github/wiki/SECURITY.md new file mode 100644 index 0000000..bd6045c --- /dev/null +++ b/.github/wiki/SECURITY.md @@ -0,0 +1,31 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +| --- | --- | +| 1.x (latest) | Yes | +| < 1.0 | No | + +## Reporting a vulnerability + +moon-sighting is a pure astronomical computation library. It accepts observer coordinates and a date as input and returns visibility predictions. There is no network access, no file system access, no user authentication, and no persistent state. The JPL DE442S ephemeris data is bundled as a static binary blob. + +Security vulnerabilities are unlikely given the surface area. That said, if you find something: + +1. **Do not open a public issue.** That exposes the vulnerability before a fix is available. +2. Email **aric.camarata@gmail.com** with the subject line "Security: moon-sighting". +3. Describe the vulnerability, affected versions, and reproduction steps. +4. You will receive a response within 7 days. + +## What counts as a security issue here + +- An input that causes the library to execute arbitrary code +- A dependency with a known CVE that affects this package's behavior +- Prototype pollution via user-provided inputs +- Buffer overflow or memory corruption in the ephemeris parsing code + +## What does not count + +- Incorrect crescent visibility predictions (that is a bug, not a security issue) +- Missing input validation that causes incorrect output but no code execution diff --git a/.github/wiki/_Footer.md b/.github/wiki/_Footer.md new file mode 100644 index 0000000..a828c33 --- /dev/null +++ b/.github/wiki/_Footer.md @@ -0,0 +1 @@ +[moon-sighting](https://github.com/acamarata/moon-sighting) · MIT License · [npm](https://www.npmjs.com/package/moon-sighting) · [Issues](https://github.com/acamarata/moon-sighting/issues) diff --git a/.github/wiki/_Sidebar.md b/.github/wiki/_Sidebar.md new file mode 100644 index 0000000..663f624 --- /dev/null +++ b/.github/wiki/_Sidebar.md @@ -0,0 +1,27 @@ +**[Home](Home)** + +**Guides** +- [Getting Started](Getting-Started) +- [Quick Start](guides/quickstart) +- [Advanced Usage](guides/advanced) + +**Examples** +- [Basic Usage](examples/basic-usage) + +**Domain Reference** +- [Crescent Visibility](Crescent-Visibility) +- [Observer Model](Observer-Model) +- [Ephemeris](Ephemeris) +- [Reference Frames](Reference-Frames) +- [Time Scales](Time-Scales) +- [Validation](Validation) + +**Package Reference** +- [API Reference](API-Reference) +- [Architecture](Architecture) +- [Benchmarks](benchmarks/index) + +**Community** +- [Contributing](CONTRIBUTING) +- [Code of Conduct](CODE_OF_CONDUCT) +- [Security](SECURITY) diff --git a/.github/wiki/benchmarks/index.md b/.github/wiki/benchmarks/index.md new file mode 100644 index 0000000..e0a5f59 --- /dev/null +++ b/.github/wiki/benchmarks/index.md @@ -0,0 +1,51 @@ +# Performance Benchmarks + +## Computation performance + +Measured on Node 22, Apple M2. Input: 100 observer+date combinations. + +| Operation | Time | +|---|---| +| `visibility()` — Yallop criterion | ~2.8 ms/call | +| `visibility()` — Odeh criterion | ~2.8 ms/call | +| `visibility()` — changing observer only | ~2.8 ms/call | +| Batch of 30 consecutive nights | ~84 ms total | + +The dominant cost is the JPL DE442S ephemeris evaluation (Chebyshev polynomial interpolation). The criterion evaluation (Yallop or Odeh) is negligible by comparison. Switching between criteria has no measurable effect on timing. + +For UI use cases, a single call is fast enough to run synchronously. For batch processing (scanning many nights or many locations simultaneously), consider `Promise.all` across concurrent calls or a Worker thread. + +## Bundle size + +| Module | Min+gz | +|---|---| +| moon-sighting (published package) | ~14 KB | +| DE442S kernel | ~31 MB (downloaded separately, not in npm package) | + +The ephemeris kernel is a binary file that users download at runtime (or self-host). It is not bundled in the npm package. See the [Getting Started](../Getting-Started.md) page for setup. + +## Reproducing the benchmarks + +```typescript +import { visibility } from 'moon-sighting'; + +const observer = { lat: 21.39, lng: 39.86, elevation: 277 }; +const baseDate = new Date(2023, 0, 1); + +const dates = Array.from({ length: 100 }, (_, i) => { + const d = new Date(baseDate); + d.setDate(d.getDate() + i); + return d; +}); + +const start = performance.now(); +for (const date of dates) { + visibility({ ...observer, date }); +} +const elapsed = performance.now() - start; + +console.log(`${(elapsed / dates.length).toFixed(1)} ms/call`); +console.log(`${elapsed.toFixed(0)} ms total for ${dates.length} calls`); +``` + +Run with `node --version` >= 20 after placing the DE442S kernel file in the working directory. diff --git a/.github/wiki/examples/basic-usage.md b/.github/wiki/examples/basic-usage.md new file mode 100644 index 0000000..c56ec5a --- /dev/null +++ b/.github/wiki/examples/basic-usage.md @@ -0,0 +1,96 @@ +# Basic Usage Examples + +## Basic crescent visibility check + +```typescript +import { visibility } from 'moon-sighting'; + +const result = visibility({ + date: new Date(2023, 2, 22), // March 22, 2023 (month is 0-indexed) + lat: 21.39, + lng: 39.86, + elevation: 277, +}); + +console.log(result.visible); // true or false +console.log(result.q?.toFixed(3)); // Yallop q-value, e.g. '0.216' +``` + +## Use Odeh criterion + +```typescript +import { visibility } from 'moon-sighting'; + +const result = visibility({ + date: new Date(2023, 2, 22), + lat: 40.0, + lng: -75.0, + elevation: 100, + criterion: 'odeh', +}); + +console.log(result.visible); // true or false +console.log(result.category); // Odeh category string +``` + +## Check visibility from multiple cities + +```typescript +import { visibility } from 'moon-sighting'; + +const newMoonDate = new Date(2023, 2, 22); + +const cities = [ + { name: 'Mecca', lat: 21.39, lng: 39.86, elevation: 277 }, + { name: 'London', lat: 51.51, lng: -0.13, elevation: 11 }, + { name: 'New York', lat: 40.71, lng: -74.00, elevation: 10 }, + { name: 'Karachi', lat: 24.86, lng: 67.01, elevation: 13 }, +]; + +for (const city of cities) { + const r = visibility({ ...city, date: newMoonDate }); + console.log(`${city.name}: ${r.visible ? 'visible' : 'not visible'} (q=${r.q?.toFixed(3)})`); +} +``` + +## Scan multiple nights for first visibility + +```typescript +import { visibility } from 'moon-sighting'; + +function findFirstVisible(observer: { lat: number; lng: number; elevation: number }, startDate: Date, maxDays = 5) { + for (let i = 0; i < maxDays; i++) { + const date = new Date(startDate); + date.setDate(startDate.getDate() + i); + const r = visibility({ ...observer, date }); + if (r.visible) { + return { date, result: r }; + } + } + return null; +} + +const observer = { lat: 21.39, lng: 39.86, elevation: 277 }; +const startDate = new Date(2023, 2, 21); // March 21, 2023 + +const firstVisible = findFirstVisible(observer, startDate); +if (firstVisible) { + console.log('First visible:', firstVisible.date.toDateString()); + console.log('q-value:', firstVisible.result.q?.toFixed(3)); +} +``` + +## CJS usage + +```javascript +const { visibility } = require('moon-sighting'); + +const result = visibility({ + date: new Date(2023, 2, 22), + lat: 21.39, + lng: 39.86, + elevation: 277, +}); + +console.log(result.visible); +``` diff --git a/.github/wiki/guides/advanced.md b/.github/wiki/guides/advanced.md new file mode 100644 index 0000000..551873b --- /dev/null +++ b/.github/wiki/guides/advanced.md @@ -0,0 +1,90 @@ +# Advanced Usage + +## Batch processing multiple nights + +```typescript +import { visibility } from 'moon-sighting'; + +const observer = { lat: 21.39, lng: 39.86, elevation: 277 }; + +function findFirstVisibleNight(startDate: Date, maxDays = 5) { + for (let i = 0; i < maxDays; i++) { + const date = new Date(startDate); + date.setDate(startDate.getDate() + i); + const r = visibility({ ...observer, date }); + if (r.visible) return { date, result: r }; + } + return null; +} +``` + +## Comparing criteria + +Both Yallop and Odeh criteria are supported. They often agree, but can differ near threshold cases: + +```typescript +import { visibility } from 'moon-sighting'; + +const params = { + date: new Date(2023, 2, 22), + lat: 40.0, + lng: -75.0, + elevation: 100, +}; + +const yallop = visibility({ ...params, criterion: 'yallop' }); +const odeh = visibility({ ...params, criterion: 'odeh' }); + +console.log('Yallop visible:', yallop.visible, 'q:', yallop.q?.toFixed(3)); +console.log('Odeh visible:', odeh.visible, 'category:', odeh.category); +``` + +## Working with the raw angular parameters + +The result object includes intermediate angular values for custom analysis: + +```typescript +import { visibility } from 'moon-sighting'; + +const r = visibility({ + date: new Date(2023, 2, 22), + lat: 21.39, + lng: 39.86, + elevation: 277, +}); + +// Arc of light (ARCL) and arc of vision (ARCV) in degrees +// These are the primary inputs to both the Yallop and Odeh criteria +console.log('ARCL:', r.arcl.toFixed(2), 'deg'); +console.log('ARCV:', r.arcv.toFixed(2), 'deg'); +console.log('DAZ:', r.daz.toFixed(2), 'deg'); +console.log('W:', r.w.toFixed(2), 'arcmin'); +``` + +## Elevation effect + +Observer elevation extends the visible horizon slightly. For most land-based observations, the effect on crescent visibility is small (less than a few percent of the q-value). Coastal or mountaintop observations with clear western horizon benefit most. + +```typescript +import { visibility } from 'moon-sighting'; + +const base = { date: new Date(2023, 2, 22), lat: 21.39, lng: 39.86, criterion: 'yallop' }; + +const seaLevel = visibility({ ...base, elevation: 0 }); +const mountain = visibility({ ...base, elevation: 2000 }); + +console.log('Sea level q:', seaLevel.q?.toFixed(4)); +console.log('2000m q:', mountain.q?.toFixed(4)); +``` + +## Time zone handling + +`date` is interpreted as a local Date object. For consistent results across environments, construct the date in UTC and adjust for the observer's local sunset time: + +```typescript +// March 22, 2023 at 18:30 local time in Mecca (UTC+3 = 15:30 UTC) +const date = new Date(Date.UTC(2023, 2, 22, 15, 30, 0)); +const r = visibility({ date, lat: 21.39, lng: 39.86, elevation: 277 }); +``` + +The library computes sunset and moon positions at the provided timestamp. Pass the expected observation time (around sunset) for most accurate results. diff --git a/.github/wiki/guides/quickstart.md b/.github/wiki/guides/quickstart.md new file mode 100644 index 0000000..5d6e227 --- /dev/null +++ b/.github/wiki/guides/quickstart.md @@ -0,0 +1,97 @@ +# Quick Start + +This guide covers the most common use cases in moon-sighting. + +## Installation + +```bash +pnpm add moon-sighting +``` + +No peer dependencies. The JPL DE442S ephemeris is bundled. + +## Check crescent visibility for a specific date and location + +```typescript +import { visibility } from 'moon-sighting'; + +const result = visibility({ + date: new Date(2023, 2, 22), // March 22, 2023 — evening of 29 Sha'ban 1444 + lat: 21.3891, // Mecca, Saudi Arabia + lng: 39.8579, + elevation: 277, // meters above sea level +}); + +console.log(result.criterion); // 'yallop' (default) +console.log(result.visible); // true or false +console.log(result.q); // Yallop q-value +console.log(result.category); // 'A' through 'F' (Yallop) or 0/1/2 (Odeh) +``` + +## Use the Odeh criterion + +```typescript +import { visibility } from 'moon-sighting'; + +const result = visibility({ + date: new Date(2023, 2, 22), + lat: 51.5, + lng: -0.12, + criterion: 'odeh', +}); + +console.log(result.category); // 0=invisible, 1=optical aid, 2=naked eye +``` + +## Batch: check a range of dates + +```typescript +import { visibility } from 'moon-sighting'; + +const observer = { lat: 40.71, lng: -74.01, elevation: 10 }; + +// Check every night for 5 days +const dates = Array.from({ length: 5 }, (_, i) => { + const d = new Date(2023, 2, 20); + d.setDate(d.getDate() + i); + return d; +}); + +for (const date of dates) { + const r = visibility({ ...observer, date }); + console.log(`${date.toDateString()}: visible=${r.visible}, q=${r.q?.toFixed(3)}`); +} +``` + +## CommonJS + +```js +const { visibility } = require('moon-sighting'); + +const result = visibility({ + date: new Date(2023, 2, 22), + lat: 21.39, + lng: 39.86, +}); + +console.log(result.visible); +``` + +## Result fields + +| Field | Type | Description | +| ----------- | --------- | ---------------------------------------------------- | +| `visible` | `boolean` | Whether the crescent is likely visible | +| `criterion` | `string` | `'yallop'` or `'odeh'` | +| `q` | `number` | Yallop q-value (only when criterion is `'yallop'`) | +| `category` | `string` | Yallop A-F or Odeh 0/1/2 category | +| `arcl` | `number` | Arc of light (degrees) | +| `arcv` | `number` | Arc of vision (degrees) | +| `daz` | `number` | Relative azimuth (degrees) | +| `w` | `number` | Crescent width (arcminutes) | + +## Next steps + +- [API Reference](API-Reference) for the full input/output schema +- [Crescent Visibility](Crescent-Visibility) for a detailed explanation of the Yallop and Odeh criteria +- [Observer Model](Observer-Model) for how observer location affects calculations diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8df123..a02369a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,30 +15,28 @@ jobs: node: [20, 22, 24] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: pnpm + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm build - run: node --test test.mjs - run: node --test test-cjs.cjs lint: - name: Lint + name: Lint & Format runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm run lint - run: pnpm run format:check @@ -48,13 +46,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm run typecheck @@ -63,13 +60,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm build - name: Verify pack contents diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml index 48deda3..245a281 100644 --- a/.github/workflows/wiki-sync.yml +++ b/.github/workflows/wiki-sync.yml @@ -1,4 +1,4 @@ -name: Sync Wiki +name: Wiki Sync on: push: @@ -6,18 +6,20 @@ on: paths: - '.github/wiki/**' +permissions: + contents: write + jobs: sync: - name: Sync .github/wiki/ to GitHub Wiki + name: Sync wiki to GitHub Wiki runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Sync wiki pages - uses: newrelic/wiki-sync-action@v1.0.1 + - name: Sync .github/wiki/ to GitHub Wiki + uses: Andrew-Chen-Wang/github-wiki-action@v4 with: - source: .wiki - destination: wiki - token: ${{ secrets.GITHUB_TOKEN }} - gitAuthorName: github-actions[bot] - gitAuthorEmail: 41898282+github-actions[bot]@users.noreply.github.com + path: .github/wiki/ + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR: ${{ github.actor }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fda38a1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.1.0] - 2026-05-28 + +### Added +- Initial release diff --git a/package.json b/package.json index 5395b83..b9bf4df 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,11 @@ "types": "./dist/index.d.ts", "exports": { ".": { - "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" - }, - "require": { - "types": "./dist/index.d.ts", - "default": "./dist/index.cjs" - } - } + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" }, "bin": { "moon-sighting": "dist/cli/index.cjs" @@ -41,7 +37,8 @@ "format": "prettier --write src/", "format:check": "prettier --check src/", "prepublishOnly": "tsup", - "cli": "node dist/cli/index.cjs" + "cli": "node dist/cli/index.cjs", + "coverage": "c8 --reporter=lcov --reporter=text node --test" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -83,5 +80,7 @@ "sighting", "ramadan", "prayer-times" - ] + ], + "type": "module", + "packageManager": "pnpm@10.11.1" }