@aulunarcana/ephemeris 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,46 @@
1
+ AuLun Ephemeris — Proprietary License
2
+ Free for Non-Commercial Use
3
+
4
+ Copyright (c) 2026 AuLun Arcana LLC. All rights reserved.
5
+
6
+ This license governs the compiled AuLun Ephemeris package (the
7
+ "Software"): the WASM binary, the JavaScript/TypeScript wrapper, and
8
+ the embedded astronomical data as distributed. Source code is not
9
+ included and is not licensed.
10
+
11
+ 1. NON-COMMERCIAL GRANT. You may install and use the Software free of
12
+ charge for personal, educational, research, and other
13
+ non-commercial purposes. "Non-commercial" excludes any use in or
14
+ for a product, service, or activity that is sold, monetized, or
15
+ operated for commercial advantage, whether or not for profit.
16
+
17
+ 2. COMMERCIAL USE. Any commercial use requires a separate commercial
18
+ license from the copyright holder. Contact:
19
+ https://github.com/routerbad
20
+
21
+ 3. RESTRICTIONS. You may not (a) redistribute, sublicense, sell, or
22
+ host the Software for use by third parties except as an
23
+ incidental, non-separable part of a permitted non-commercial
24
+ application; (b) modify, adapt, or create derivative works of the
25
+ Software; (c) reverse engineer, decompile, or disassemble the
26
+ Software, or extract its embedded implementation, except to the
27
+ extent applicable law expressly permits despite this limitation;
28
+ (d) remove or alter proprietary notices.
29
+
30
+ 4. DATA. The astronomical data embedded in the Software derives from
31
+ public-domain and factual sources (documented by the copyright
32
+ holder); nothing in this license restricts your independent use of
33
+ those underlying public sources.
34
+
35
+ 5. OWNERSHIP. The Software is licensed, not sold. The copyright
36
+ holder retains all rights not expressly granted.
37
+
38
+ 6. TERMINATION. This license terminates automatically if you breach
39
+ it. Upon termination you must cease use and destroy copies.
40
+
41
+ 7. NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
42
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
43
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, ACCURACY, AND
44
+ NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE
45
+ FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF
46
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # @aulunarcana/ephemeris
2
+
3
+ AuLun Ephemeris — an astronomical ephemeris and natal chart engine
4
+ compiled to WebAssembly. Sub-arcsecond accuracy validated against JPL
5
+ Horizons across 1900–2100 (Moon median 0.06″). **Free for
6
+ non-commercial use**; commercial licenses available (see License
7
+ below).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @aulunarcana/ephemeris
13
+ ```
14
+
15
+ Node 18+. Self-contained — all ephemeris data (~13 MB) is embedded.
16
+
17
+ ## Quick start — tropical zodiac, Placidus houses
18
+
19
+ ```js
20
+ import { Ephemeris, degreesToRadians, radiansToDegrees } from "@aulunarcana/ephemeris";
21
+
22
+ const engine = new Ephemeris(); // parses embedded data once — construct once, reuse
23
+
24
+ const chart = engine.computeChart(
25
+ { year: 1990, month: 6, day: 15, hour: 21, minute: 30 }, // local civil time
26
+ "America/Los_Angeles", // IANA timezone
27
+ { latitudeRad: degreesToRadians(34.05), longitudeRad: degreesToRadians(-118.24) },
28
+ { houseSystem: "placidus" }, // tropical is the default zodiac
29
+ );
30
+
31
+ radiansToDegrees(chart.ascendantRad); // Ascendant
32
+ chart.bodies.sun.longitudeRad; // apparent ecliptic longitude
33
+ chart.bodies.moon.longitudeRateRadPerDay; // negative = retrograde
34
+ chart.houses.cuspsRad; // the 12 Placidus cusps
35
+ chart.aspects; // aspects with applying/separating
36
+ chart.patterns; // grand trines, T-squares, …
37
+ ```
38
+
39
+ All angles are radians; convert with `radiansToDegrees` /
40
+ `degreesToRadians`. The full result shape is in `index.d.ts`
41
+ (`Chart`).
42
+
43
+ ## What a chart contains
44
+
45
+ - **Bodies** (`chart.bodies`): `sun`, `moon`, `mercury`, `venus`,
46
+ `mars`, `jupiter`, `saturn`, `uranus`, `neptune`, `pluto`,
47
+ `chiron` — each with apparent ecliptic longitude, latitude,
48
+ distance, and daily motion (negative = retrograde).
49
+ - **Angles and houses**: Ascendant, Midheaven, 12 house cusps.
50
+ - **Lunar points**: mean and true North/South Node and Lilith.
51
+ - **Hermetic lots**: Fortune, Spirit, Eros, Necessity, Courage,
52
+ Victory, Nemesis (day/night sect handled via `isDayChart`).
53
+ - **Aspects**: major and minor aspects with orb, exact angle, and
54
+ applying/separating; plus detected patterns (grand trine,
55
+ T-square, …).
56
+
57
+ ## House systems
58
+
59
+ Pass one of these as `houseSystem` (default: `"placidus"`):
60
+
61
+ | Value | System |
62
+ |---|---|
63
+ | `"placidus"` | Placidus |
64
+ | `"koch"` | Koch |
65
+ | `"whole_sign"` | Whole Sign |
66
+ | `"equal"` | Equal |
67
+ | `"porphyry"` | Porphyry |
68
+ | `"regiomontanus"` | Regiomontanus |
69
+ | `"campanus"` | Campanus |
70
+ | `"alcabitius"` | Alcabitius |
71
+ | `"topocentric"` | Topocentric (Polich–Page) |
72
+ | `"morinus"` | Morinus |
73
+ | `"meridian"` | Meridian (Axial) |
74
+
75
+ ## Zodiac: tropical and sidereal (Vedic)
76
+
77
+ Every chart is computed in the **tropical** zodiac. To also get
78
+ sidereal values, pass an `ayanamsa`:
79
+
80
+ | Value | Tradition |
81
+ |---|---|
82
+ | `"lahiri"` | Vedic / Jyotish standard (Chitrapaksha) |
83
+ | `"fagan_bradley"` | Western sidereal |
84
+
85
+ ```js
86
+ const vedic = engine.computeChart(civil, zone, observer, {
87
+ houseSystem: "whole_sign",
88
+ ayanamsa: "lahiri",
89
+ });
90
+ vedic.bodies.moon.siderealLongitudeRad; // sidereal position
91
+ vedic.siderealHouses.cuspsRad; // sidereal cusps
92
+ vedic.ayanamsaRad; // the ayanamsa applied
93
+ ```
94
+
95
+ Sidereal output is additive — the tropical fields are always present.
96
+
97
+ ## Extended minor bodies (optional)
98
+
99
+ Sixteen additional bodies are supported but not embedded: `ceres`,
100
+ `pallas`, `juno`, `vesta`, `eris`, `sedna`, `haumea`, `makemake`,
101
+ `quaoar`, `orcus`, `varuna`, `ixion`, `gonggong`, `pholus`, `nessus`,
102
+ `chariklo`. Load a body's data table first, then request it:
103
+
104
+ ```js
105
+ engine.loadMinorBodyTable("ceres", ceresTableText);
106
+ const chart = engine.computeChart(civil, zone, observer, {
107
+ includeMinorBodies: ["ceres"],
108
+ });
109
+ ```
110
+
111
+ A requested body whose table isn't loaded (or whose date is outside
112
+ table coverage) is listed in `chart.outOfRange` instead of failing
113
+ the chart.
114
+
115
+ ## Time handling
116
+
117
+ `computeChart` takes naive civil time plus an IANA zone and resolves
118
+ DST correctly:
119
+
120
+ - Ambiguous local times (DST fall-back) throw
121
+ `AmbiguousLocalTimeError` unless you pass `fold` (`0` = earlier
122
+ instant, `1` = later).
123
+ - Nonexistent local times (spring-forward gap) always throw
124
+ `NonexistentLocalTimeError`.
125
+
126
+ If you already have UTC, use
127
+ `engine.computeChartUtc(utc, observer, options)` and skip timezone
128
+ resolution entirely.
129
+
130
+ ## Accuracy
131
+
132
+ Apparent positions are computed from VSOP87E, ELP/MPP02, and
133
+ JPL-derived state vectors with full IAU 2000A/2006 precession-
134
+ nutation, light-time, and relativistic aberration. Validated against
135
+ JPL Horizons over 1900–2100: sub-arcsecond median accuracy for every
136
+ supported body.
137
+
138
+ ## License
139
+
140
+ Proprietary. **Free for non-commercial use** — personal, educational,
141
+ and research use is free of charge under the LICENSE shipped with
142
+ this package. Use in or for anything sold, monetized, or operated for
143
+ commercial advantage requires a commercial license: contact the
144
+ copyright holder via https://github.com/AuLunArcana.
145
+
146
+ © 2026 AuLun Arcana LLC. All rights reserved.
package/index.d.ts ADDED
@@ -0,0 +1,158 @@
1
+ /** Type surface for @aulunarcana/ephemeris. All angles are radians
2
+ * unless a name says otherwise; use radiansToDegrees for display. */
3
+
4
+ export interface CivilTime {
5
+ year: number;
6
+ month: number;
7
+ day: number;
8
+ hour: number;
9
+ minute: number;
10
+ second?: number;
11
+ }
12
+
13
+ export interface UtcFields {
14
+ year: number;
15
+ month: number;
16
+ day: number;
17
+ hour: number;
18
+ minute: number;
19
+ second: number;
20
+ }
21
+
22
+ export interface Observer {
23
+ latitudeRad: number;
24
+ longitudeRad: number;
25
+ heightM?: number;
26
+ }
27
+
28
+ export type HouseSystem =
29
+ | "whole_sign"
30
+ | "equal"
31
+ | "porphyry"
32
+ | "meridian"
33
+ | "regiomontanus"
34
+ | "campanus"
35
+ | "alcabitius"
36
+ | "placidus"
37
+ | "koch"
38
+ | "topocentric"
39
+ | "morinus";
40
+
41
+ export type Ayanamsa = "lahiri" | "fagan_bradley";
42
+
43
+ export interface ChartOptions {
44
+ houseSystem?: HouseSystem;
45
+ includeMinorBodies?: string[];
46
+ ayanamsa?: Ayanamsa;
47
+ /** null rejects ambiguous local times; 0 = earlier, 1 = later. */
48
+ fold?: 0 | 1 | null;
49
+ }
50
+
51
+ export interface BodyPosition {
52
+ longitudeRad: number;
53
+ latitudeRad: number;
54
+ distanceAu: number;
55
+ longitudeRateRadPerDay: number;
56
+ siderealLongitudeRad: number | null;
57
+ }
58
+
59
+ export interface Houses {
60
+ ascendantRad: number;
61
+ midheavenRad: number;
62
+ cuspsRad: number[];
63
+ }
64
+
65
+ export interface AspectHit {
66
+ bodyA: string;
67
+ bodyB: string;
68
+ aspect: string;
69
+ exactAngleRad: number;
70
+ separationRad: number;
71
+ orbRad: number;
72
+ applying: boolean | null;
73
+ }
74
+
75
+ export interface PatternHit {
76
+ pattern: string;
77
+ bodies: string[];
78
+ }
79
+
80
+ export interface LunarPoints {
81
+ meanNodeRad: number;
82
+ trueNodeRad: number;
83
+ meanSouthNodeRad: number;
84
+ trueSouthNodeRad: number;
85
+ meanLilithRad: number;
86
+ trueLilithRad: number;
87
+ }
88
+
89
+ export interface Lots {
90
+ fortune: number;
91
+ spirit: number;
92
+ eros: number;
93
+ necessity: number;
94
+ courage: number;
95
+ victory: number;
96
+ nemesis: number;
97
+ }
98
+
99
+ export interface Chart {
100
+ jdUtc: number;
101
+ jdTt: number;
102
+ houseSystem: string;
103
+ ayanamsaName: string | null;
104
+ ayanamsaRad: number | null;
105
+ bodies: Record<string, BodyPosition>;
106
+ outOfRange: string[];
107
+ ascendantRad: number;
108
+ midheavenRad: number;
109
+ houses: Houses;
110
+ siderealHouses: Houses | null;
111
+ lunarPoints: LunarPoints;
112
+ isDayChart: boolean;
113
+ lots: Lots;
114
+ aspects: AspectHit[];
115
+ patterns: PatternHit[];
116
+ utc?: UtcFields;
117
+ ianaZone?: string;
118
+ }
119
+
120
+ export class AmbiguousLocalTimeError extends Error {}
121
+ export class NonexistentLocalTimeError extends Error {}
122
+
123
+ export function resolveCivilToUtc(
124
+ local: CivilTime,
125
+ zone: string,
126
+ fold?: 0 | 1 | null,
127
+ ): UtcFields;
128
+
129
+ export function radiansToDegrees(rad: number): number;
130
+ export function degreesToRadians(deg: number): number;
131
+
132
+ export interface BodyPositionLite {
133
+ longitudeRad: number;
134
+ latitudeRad: number;
135
+ distanceAu: number;
136
+ longitudeRateRadPerDay: number;
137
+ }
138
+
139
+ export interface BodyPositions {
140
+ jdUtc: number;
141
+ jdTt: number;
142
+ bodies: Record<string, BodyPositionLite>;
143
+ outOfRange: string[];
144
+ }
145
+
146
+ export class Ephemeris {
147
+ constructor();
148
+ loadMinorBodyTable(name: string, content: string): void;
149
+ computeChart(
150
+ civil: CivilTime,
151
+ zone: string,
152
+ observer: Observer,
153
+ options?: ChartOptions,
154
+ ): Chart;
155
+ computeChartUtc(utc: UtcFields, observer: Observer, options?: ChartOptions): Chart;
156
+ /** Positions-only fast path for a subset of bodies (transit scans). */
157
+ computeBodies(utc: Omit<UtcFields, "second"> & { second?: number }, bodies: string[]): BodyPositions;
158
+ }
package/index.mjs ADDED
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @aulunarcana/ephemeris — JS/TS surface over the WASM engine.
3
+ *
4
+ * Civil-time resolution lives HERE (port design decision 1): the WASM
5
+ * core takes a resolved UTC instant. Resolution uses Temporal with
6
+ * explicit disambiguation when the runtime ships it, and otherwise an
7
+ * Intl.DateTimeFormat offset-prober implementing the same semantics as
8
+ * the Python prototype's resolve_civil_datetime_to_utc:
9
+ * - fold null on an ambiguous local time (DST fall-back): throws
10
+ * AmbiguousLocalTimeError; fold 0 selects the earlier UTC instant,
11
+ * fold 1 the later.
12
+ * - a nonexistent local time (spring-forward gap) throws
13
+ * NonexistentLocalTimeError regardless of fold.
14
+ * Conformance is tested against Python-generated fold vectors
15
+ * (test/fold_vectors.json).
16
+ */
17
+ import { createRequire } from "node:module";
18
+ const require = createRequire(import.meta.url);
19
+ const wasm = require("./pkg/ephemeris_wasm.js");
20
+
21
+ export class AmbiguousLocalTimeError extends Error {}
22
+ export class NonexistentLocalTimeError extends Error {}
23
+
24
+ const DAY_MS = 86400000;
25
+
26
+ function wallFieldsAt(epochMs, zone) {
27
+ const dtf = new Intl.DateTimeFormat("en-US", {
28
+ timeZone: zone,
29
+ era: "short",
30
+ year: "numeric",
31
+ month: "2-digit",
32
+ day: "2-digit",
33
+ hour: "2-digit",
34
+ minute: "2-digit",
35
+ second: "2-digit",
36
+ hour12: false,
37
+ });
38
+ const parts = Object.fromEntries(
39
+ dtf.formatToParts(new Date(epochMs)).map((p) => [p.type, p.value]),
40
+ );
41
+ let year = Number(parts.year);
42
+ if (parts.era === "BC") year = 1 - year;
43
+ return {
44
+ year,
45
+ month: Number(parts.month),
46
+ day: Number(parts.day),
47
+ hour: Number(parts.hour) % 24, // some ICU builds format midnight as "24"
48
+ minute: Number(parts.minute),
49
+ second: Number(parts.second),
50
+ };
51
+ }
52
+
53
+ function fieldsAsUtcMs(f) {
54
+ // NOT Date.UTC(...): it silently remaps years 0-99 to 1900-1999
55
+ // (review finding). setUTCFullYear takes the year literally, so
56
+ // ancient/first-century dates resolve correctly, matching both the
57
+ // Python prototype and the Temporal path.
58
+ const d = new Date(0);
59
+ d.setUTCFullYear(f.year, f.month - 1, f.day);
60
+ d.setUTCHours(f.hour, f.minute, f.second ?? 0, 0);
61
+ return d.getTime();
62
+ }
63
+
64
+ function zoneOffsetMs(epochMs, zone) {
65
+ return fieldsAsUtcMs(wallFieldsAt(epochMs, zone)) - epochMs;
66
+ }
67
+
68
+ function msToUtcFields(epochMs) {
69
+ const d = new Date(epochMs);
70
+ return {
71
+ year: d.getUTCFullYear(),
72
+ month: d.getUTCMonth() + 1,
73
+ day: d.getUTCDate(),
74
+ hour: d.getUTCHours(),
75
+ minute: d.getUTCMinutes(),
76
+ second: d.getUTCSeconds(),
77
+ };
78
+ }
79
+
80
+ function resolveViaIntl(local, zone, fold) {
81
+ const guess = fieldsAsUtcMs(local);
82
+ // Probe the zone offset around the guess: any transition within a
83
+ // day of the target surfaces both candidate offsets.
84
+ const offsets = new Set([
85
+ zoneOffsetMs(guess - DAY_MS, zone),
86
+ zoneOffsetMs(guess, zone),
87
+ zoneOffsetMs(guess + DAY_MS, zone),
88
+ ]);
89
+ const candidates = [];
90
+ for (const offset of offsets) {
91
+ const instant = guess - offset;
92
+ // Valid iff the round trip reproduces the requested wall time.
93
+ if (zoneOffsetMs(instant, zone) === offset) {
94
+ candidates.push(instant);
95
+ }
96
+ }
97
+ const unique = [...new Set(candidates)].sort((a, b) => a - b);
98
+ if (unique.length === 0) {
99
+ throw new NonexistentLocalTimeError(
100
+ `${JSON.stringify(local)} in ${zone} does not exist (DST spring-forward gap).`,
101
+ );
102
+ }
103
+ if (unique.length === 1) {
104
+ return msToUtcFields(unique[0]);
105
+ }
106
+ if (fold === null || fold === undefined) {
107
+ throw new AmbiguousLocalTimeError(
108
+ `${JSON.stringify(local)} in ${zone} is ambiguous (DST fall-back); ` +
109
+ "pass fold 0 (earlier) or 1 (later) explicitly.",
110
+ );
111
+ }
112
+ return msToUtcFields(fold === 0 ? unique[0] : unique[unique.length - 1]);
113
+ }
114
+
115
+ function resolveViaTemporal(local, zone, fold) {
116
+ const plain = new Temporal.PlainDateTime(
117
+ local.year,
118
+ local.month,
119
+ local.day,
120
+ local.hour,
121
+ local.minute,
122
+ local.second ?? 0,
123
+ );
124
+ let zoned;
125
+ try {
126
+ zoned = plain.toZonedDateTime(zone, {
127
+ disambiguation:
128
+ fold === null || fold === undefined
129
+ ? "reject"
130
+ : fold === 0
131
+ ? "earlier"
132
+ : "later",
133
+ });
134
+ } catch (error) {
135
+ // Temporal's reject covers BOTH gap and ambiguity; a Python-style
136
+ // round-trip re-probe classifies which, and the gap case must
137
+ // throw even with an explicit fold.
138
+ return classifyRejection(local, zone, fold);
139
+ }
140
+ // Temporal resolves gap times under earlier/later; Python raises for
141
+ // gaps regardless of fold. Round-trip check preserves that contract.
142
+ const roundTrip = zoned.toPlainDateTime();
143
+ if (
144
+ roundTrip.year !== plain.year ||
145
+ roundTrip.month !== plain.month ||
146
+ roundTrip.day !== plain.day ||
147
+ roundTrip.hour !== plain.hour ||
148
+ roundTrip.minute !== plain.minute ||
149
+ roundTrip.second !== plain.second
150
+ ) {
151
+ throw new NonexistentLocalTimeError(
152
+ `${JSON.stringify(local)} in ${zone} does not exist (DST spring-forward gap).`,
153
+ );
154
+ }
155
+ return msToUtcFields(zoned.epochMilliseconds);
156
+ }
157
+
158
+ function classifyRejection(local, zone, fold) {
159
+ // Temporal's 'reject' throws for BOTH gap and ambiguity; the Intl
160
+ // prober re-derives which, throwing our typed error (or resolving,
161
+ // if Temporal rejected something the prober can settle).
162
+ return resolveViaIntl(local, zone, fold);
163
+ }
164
+
165
+ /**
166
+ * Resolve a naive local civil time + IANA zone to UTC fields.
167
+ * fold: null/undefined (reject ambiguity), 0 (earlier), 1 (later).
168
+ */
169
+ export function resolveCivilToUtc(local, zone, fold = null) {
170
+ if (typeof globalThis.Temporal !== "undefined") {
171
+ return resolveViaTemporal(local, zone, fold);
172
+ }
173
+ return resolveViaIntl(local, zone, fold);
174
+ }
175
+
176
+ const RAD_TO_DEG = 180 / Math.PI;
177
+ export function radiansToDegrees(rad) {
178
+ return rad * RAD_TO_DEG;
179
+ }
180
+ export function degreesToRadians(deg) {
181
+ return deg / RAD_TO_DEG;
182
+ }
183
+
184
+ /**
185
+ * The one-call chart surface. Construct once (parses ~13 MB of
186
+ * embedded ephemeris data), then computeChart per birth.
187
+ */
188
+ export class Ephemeris {
189
+ constructor() {
190
+ this.engine = new wasm.Engine();
191
+ }
192
+
193
+ /** Load one extended minor body's vendored table text at runtime. */
194
+ loadMinorBodyTable(name, content) {
195
+ this.engine.loadMinorBodyTable(name, content);
196
+ }
197
+
198
+ /**
199
+ * civil: {year, month, day, hour, minute, second?}; zone: IANA name;
200
+ * observer: {latitudeRad, longitudeRad, heightM?} (radians — use
201
+ * degreesToRadians for degree inputs); options: {houseSystem?,
202
+ * includeMinorBodies?, ayanamsa?, fold?}.
203
+ * Returns the chart object (all angles radians) with utc attached.
204
+ */
205
+ computeChart(civil, zone, observer, options = {}) {
206
+ const utc = resolveCivilToUtc(civil, zone, options.fold ?? null);
207
+ const chart = this.computeChartUtc(utc, observer, options);
208
+ chart.utc = utc;
209
+ chart.ianaZone = zone;
210
+ return chart;
211
+ }
212
+
213
+ /** Same, from an already-resolved UTC instant. */
214
+ computeChartUtc(utc, observer, options = {}) {
215
+ const request = JSON.stringify({
216
+ utc,
217
+ observer,
218
+ options: {
219
+ houseSystem: options.houseSystem,
220
+ includeMinorBodies: options.includeMinorBodies,
221
+ ayanamsa: options.ayanamsa,
222
+ },
223
+ });
224
+ return JSON.parse(this.engine.computeChartJson(request));
225
+ }
226
+
227
+ /**
228
+ * Positions-only fast path: apparent geocentric longitude/latitude/
229
+ * distance/rate for just the requested bodies (same numbers as the
230
+ * full chart's `bodies`), skipping houses, lunar points, lots,
231
+ * aspects, and patterns. Built for transit scans that sample a few
232
+ * slow bodies across thousands of instants — a one-body query costs
233
+ * a fraction of a full chart. utc: resolved UTC fields; bodies:
234
+ * array of body names (chart bodies and loaded minors).
235
+ */
236
+ computeBodies(utc, bodies) {
237
+ const request = JSON.stringify({
238
+ utc: { second: 0, ...utc },
239
+ bodies,
240
+ });
241
+ return JSON.parse(this.engine.computeBodiesJson(request));
242
+ }
243
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@aulunarcana/ephemeris",
3
+ "version": "0.1.0",
4
+ "description": "AuLun Ephemeris: NASA-grade astronomical ephemeris and chart engine (WASM). Free for non-commercial use; commercial licenses available.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/AuLunArcana/ephemeris.git"
9
+ },
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "main": "index.mjs",
12
+ "types": "index.d.ts",
13
+ "files": ["index.mjs", "index.d.ts", "pkg/", "LICENSE"],
14
+ "scripts": {
15
+ "prepack": "cd .. && wasm-pack build --target nodejs --release --out-dir js/pkg && node -e \"fs.rmSync('js/pkg/.gitignore',{force:true})\"",
16
+ "test": "node test/smoke.mjs"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ }
21
+ }
package/pkg/LICENSE ADDED
@@ -0,0 +1,46 @@
1
+ AuLun Ephemeris — Proprietary License
2
+ Free for Non-Commercial Use
3
+
4
+ Copyright (c) 2026 AuLun Arcana LLC. All rights reserved.
5
+
6
+ This license governs the compiled AuLun Ephemeris package (the
7
+ "Software"): the WASM binary, the JavaScript/TypeScript wrapper, and
8
+ the embedded astronomical data as distributed. Source code is not
9
+ included and is not licensed.
10
+
11
+ 1. NON-COMMERCIAL GRANT. You may install and use the Software free of
12
+ charge for personal, educational, research, and other
13
+ non-commercial purposes. "Non-commercial" excludes any use in or
14
+ for a product, service, or activity that is sold, monetized, or
15
+ operated for commercial advantage, whether or not for profit.
16
+
17
+ 2. COMMERCIAL USE. Any commercial use requires a separate commercial
18
+ license from the copyright holder. Contact:
19
+ https://github.com/routerbad
20
+
21
+ 3. RESTRICTIONS. You may not (a) redistribute, sublicense, sell, or
22
+ host the Software for use by third parties except as an
23
+ incidental, non-separable part of a permitted non-commercial
24
+ application; (b) modify, adapt, or create derivative works of the
25
+ Software; (c) reverse engineer, decompile, or disassemble the
26
+ Software, or extract its embedded implementation, except to the
27
+ extent applicable law expressly permits despite this limitation;
28
+ (d) remove or alter proprietary notices.
29
+
30
+ 4. DATA. The astronomical data embedded in the Software derives from
31
+ public-domain and factual sources (documented by the copyright
32
+ holder); nothing in this license restricts your independent use of
33
+ those underlying public sources.
34
+
35
+ 5. OWNERSHIP. The Software is licensed, not sold. The copyright
36
+ holder retains all rights not expressly granted.
37
+
38
+ 6. TERMINATION. This license terminates automatically if you breach
39
+ it. Upon termination you must cease use and destroy copies.
40
+
41
+ 7. NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
42
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
43
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, ACCURACY, AND
44
+ NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE
45
+ FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF
46
+ THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ export class Engine {
5
+ free(): void;
6
+ [Symbol.dispose](): void;
7
+ /**
8
+ * Positions-only fast path: apparent geocentric longitude,
9
+ * latitude, distance, and daily rate for a caller-chosen subset
10
+ * of bodies. Same numbers as the full chart's `bodies` (same
11
+ * pipeline), skipping houses/lunar points/lots/aspects/patterns
12
+ * entirely -- built for transit scans that sample one or a few
13
+ * slow bodies across thousands of instants. Input JSON:
14
+ * {"utc": {...}, "bodies": ["jupiter", ...]}.
15
+ */
16
+ computeBodiesJson(request_json: string): string;
17
+ /**
18
+ * Computes a chart from a RESOLVED UTC instant. Input/output are
19
+ * JSON strings (the JS wrapper owns object conversion, degree
20
+ * helpers, and civil-time resolution). All angles radians,
21
+ * tropical true-ecliptic-of-date; sidereal fields additive.
22
+ */
23
+ computeChartJson(request_json: string): string;
24
+ /**
25
+ * Loads one extended minor body's vendored table content
26
+ * (data/minor_bodies/<name>_vectors.txt) at runtime -- the 16
27
+ * extended minors are not embedded. Charts requesting an
28
+ * unloaded minor degrade it into `out_of_range` (M3).
29
+ */
30
+ loadMinorBodyTable(name: string, content: string): void;
31
+ /**
32
+ * Parses every embedded dataset once (do this at startup, not
33
+ * per chart).
34
+ */
35
+ constructor();
36
+ }
@@ -0,0 +1,218 @@
1
+ /* @ts-self-types="./ephemeris_wasm.d.ts" */
2
+
3
+ class Engine {
4
+ __destroy_into_raw() {
5
+ const ptr = this.__wbg_ptr;
6
+ this.__wbg_ptr = 0;
7
+ EngineFinalization.unregister(this);
8
+ return ptr;
9
+ }
10
+ free() {
11
+ const ptr = this.__destroy_into_raw();
12
+ wasm.__wbg_engine_free(ptr, 0);
13
+ }
14
+ /**
15
+ * Positions-only fast path: apparent geocentric longitude,
16
+ * latitude, distance, and daily rate for a caller-chosen subset
17
+ * of bodies. Same numbers as the full chart's `bodies` (same
18
+ * pipeline), skipping houses/lunar points/lots/aspects/patterns
19
+ * entirely -- built for transit scans that sample one or a few
20
+ * slow bodies across thousands of instants. Input JSON:
21
+ * {"utc": {...}, "bodies": ["jupiter", ...]}.
22
+ * @param {string} request_json
23
+ * @returns {string}
24
+ */
25
+ computeBodiesJson(request_json) {
26
+ let deferred3_0;
27
+ let deferred3_1;
28
+ try {
29
+ const ptr0 = passStringToWasm0(request_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
30
+ const len0 = WASM_VECTOR_LEN;
31
+ const ret = wasm.engine_computeBodiesJson(this.__wbg_ptr, ptr0, len0);
32
+ var ptr2 = ret[0];
33
+ var len2 = ret[1];
34
+ if (ret[3]) {
35
+ ptr2 = 0; len2 = 0;
36
+ throw takeFromExternrefTable0(ret[2]);
37
+ }
38
+ deferred3_0 = ptr2;
39
+ deferred3_1 = len2;
40
+ return getStringFromWasm0(ptr2, len2);
41
+ } finally {
42
+ wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
43
+ }
44
+ }
45
+ /**
46
+ * Computes a chart from a RESOLVED UTC instant. Input/output are
47
+ * JSON strings (the JS wrapper owns object conversion, degree
48
+ * helpers, and civil-time resolution). All angles radians,
49
+ * tropical true-ecliptic-of-date; sidereal fields additive.
50
+ * @param {string} request_json
51
+ * @returns {string}
52
+ */
53
+ computeChartJson(request_json) {
54
+ let deferred3_0;
55
+ let deferred3_1;
56
+ try {
57
+ const ptr0 = passStringToWasm0(request_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
58
+ const len0 = WASM_VECTOR_LEN;
59
+ const ret = wasm.engine_computeChartJson(this.__wbg_ptr, ptr0, len0);
60
+ var ptr2 = ret[0];
61
+ var len2 = ret[1];
62
+ if (ret[3]) {
63
+ ptr2 = 0; len2 = 0;
64
+ throw takeFromExternrefTable0(ret[2]);
65
+ }
66
+ deferred3_0 = ptr2;
67
+ deferred3_1 = len2;
68
+ return getStringFromWasm0(ptr2, len2);
69
+ } finally {
70
+ wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
71
+ }
72
+ }
73
+ /**
74
+ * Loads one extended minor body's vendored table content
75
+ * (data/minor_bodies/<name>_vectors.txt) at runtime -- the 16
76
+ * extended minors are not embedded. Charts requesting an
77
+ * unloaded minor degrade it into `out_of_range` (M3).
78
+ * @param {string} name
79
+ * @param {string} content
80
+ */
81
+ loadMinorBodyTable(name, content) {
82
+ const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
83
+ const len0 = WASM_VECTOR_LEN;
84
+ const ptr1 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
85
+ const len1 = WASM_VECTOR_LEN;
86
+ const ret = wasm.engine_loadMinorBodyTable(this.__wbg_ptr, ptr0, len0, ptr1, len1);
87
+ if (ret[1]) {
88
+ throw takeFromExternrefTable0(ret[0]);
89
+ }
90
+ }
91
+ /**
92
+ * Parses every embedded dataset once (do this at startup, not
93
+ * per chart).
94
+ */
95
+ constructor() {
96
+ const ret = wasm.engine_new();
97
+ if (ret[2]) {
98
+ throw takeFromExternrefTable0(ret[1]);
99
+ }
100
+ this.__wbg_ptr = ret[0];
101
+ EngineFinalization.register(this, this.__wbg_ptr, this);
102
+ return this;
103
+ }
104
+ }
105
+ if (Symbol.dispose) Engine.prototype[Symbol.dispose] = Engine.prototype.free;
106
+ exports.Engine = Engine;
107
+ function __wbg_get_imports() {
108
+ const import0 = {
109
+ __proto__: null,
110
+ __wbg_Error_92b29b0548f8b746: function(arg0, arg1) {
111
+ const ret = Error(getStringFromWasm0(arg0, arg1));
112
+ return ret;
113
+ },
114
+ __wbg___wbindgen_throw_344f42d3211c4765: function(arg0, arg1) {
115
+ throw new Error(getStringFromWasm0(arg0, arg1));
116
+ },
117
+ __wbindgen_init_externref_table: function() {
118
+ const table = wasm.__wbindgen_externrefs;
119
+ const offset = table.grow(4);
120
+ table.set(0, undefined);
121
+ table.set(offset + 0, undefined);
122
+ table.set(offset + 1, null);
123
+ table.set(offset + 2, true);
124
+ table.set(offset + 3, false);
125
+ },
126
+ };
127
+ return {
128
+ __proto__: null,
129
+ "./ephemeris_wasm_bg.js": import0,
130
+ };
131
+ }
132
+
133
+ const EngineFinalization = (typeof FinalizationRegistry === 'undefined')
134
+ ? { register: () => {}, unregister: () => {} }
135
+ : new FinalizationRegistry(ptr => wasm.__wbg_engine_free(ptr, 1));
136
+
137
+ function getStringFromWasm0(ptr, len) {
138
+ return decodeText(ptr >>> 0, len);
139
+ }
140
+
141
+ let cachedUint8ArrayMemory0 = null;
142
+ function getUint8ArrayMemory0() {
143
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
144
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
145
+ }
146
+ return cachedUint8ArrayMemory0;
147
+ }
148
+
149
+ function passStringToWasm0(arg, malloc, realloc) {
150
+ if (realloc === undefined) {
151
+ const buf = cachedTextEncoder.encode(arg);
152
+ const ptr = malloc(buf.length, 1) >>> 0;
153
+ getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
154
+ WASM_VECTOR_LEN = buf.length;
155
+ return ptr;
156
+ }
157
+
158
+ let len = arg.length;
159
+ let ptr = malloc(len, 1) >>> 0;
160
+
161
+ const mem = getUint8ArrayMemory0();
162
+
163
+ let offset = 0;
164
+
165
+ for (; offset < len; offset++) {
166
+ const code = arg.charCodeAt(offset);
167
+ if (code > 0x7F) break;
168
+ mem[ptr + offset] = code;
169
+ }
170
+ if (offset !== len) {
171
+ if (offset !== 0) {
172
+ arg = arg.slice(offset);
173
+ }
174
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
175
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
176
+ const ret = cachedTextEncoder.encodeInto(arg, view);
177
+
178
+ offset += ret.written;
179
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
180
+ }
181
+
182
+ WASM_VECTOR_LEN = offset;
183
+ return ptr;
184
+ }
185
+
186
+ function takeFromExternrefTable0(idx) {
187
+ const value = wasm.__wbindgen_externrefs.get(idx);
188
+ wasm.__externref_table_dealloc(idx);
189
+ return value;
190
+ }
191
+
192
+ let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
193
+ cachedTextDecoder.decode();
194
+ function decodeText(ptr, len) {
195
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
196
+ }
197
+
198
+ const cachedTextEncoder = new TextEncoder();
199
+
200
+ if (!('encodeInto' in cachedTextEncoder)) {
201
+ cachedTextEncoder.encodeInto = function (arg, view) {
202
+ const buf = cachedTextEncoder.encode(arg);
203
+ view.set(buf);
204
+ return {
205
+ read: arg.length,
206
+ written: buf.length
207
+ };
208
+ };
209
+ }
210
+
211
+ let WASM_VECTOR_LEN = 0;
212
+
213
+ const wasmPath = `${__dirname}/ephemeris_wasm_bg.wasm`;
214
+ const wasmBytes = require('fs').readFileSync(wasmPath);
215
+ const wasmModule = new WebAssembly.Module(wasmBytes);
216
+ let wasmInstance = new WebAssembly.Instance(wasmModule, __wbg_get_imports());
217
+ let wasm = wasmInstance.exports;
218
+ wasm.__wbindgen_start();
Binary file
@@ -0,0 +1,14 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ export const memory: WebAssembly.Memory;
4
+ export const __wbg_engine_free: (a: number, b: number) => void;
5
+ export const engine_computeBodiesJson: (a: number, b: number, c: number) => [number, number, number, number];
6
+ export const engine_computeChartJson: (a: number, b: number, c: number) => [number, number, number, number];
7
+ export const engine_loadMinorBodyTable: (a: number, b: number, c: number, d: number, e: number) => [number, number];
8
+ export const engine_new: () => [number, number, number];
9
+ export const __wbindgen_externrefs: WebAssembly.Table;
10
+ export const __wbindgen_malloc: (a: number, b: number) => number;
11
+ export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
12
+ export const __externref_table_dealloc: (a: number) => void;
13
+ export const __wbindgen_free: (a: number, b: number, c: number) => void;
14
+ export const __wbindgen_start: () => void;
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "ephemeris-wasm",
3
+ "description": "WASM bindings for the ephemeris engine",
4
+ "version": "0.1.0",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "files": [
7
+ "ephemeris_wasm_bg.wasm",
8
+ "ephemeris_wasm.js",
9
+ "ephemeris_wasm.d.ts"
10
+ ],
11
+ "main": "ephemeris_wasm.js",
12
+ "types": "ephemeris_wasm.d.ts"
13
+ }