Codync
免费开源的 1:1 Grok Bot 与 Muse 替代方案。可在 iPhone、Mac 或 Linux 桌面上将 Claude 代码、Codex、Cursor 以及 40 多个编码代理作为持久机器人使用。支持群组聊天、线程、应用。
AI 翻译
@zodiacs/engine is the TypeScript chart engine behind zodiacs.org, MIT code with CC BY 4.0 ΔT data: natal charts, transits, synastry, Moon phase and Saturn returns, computed in your own browser or Nod
翻译准备中
开发工具TypeScript · JavaScriptGitHub @zodiacs-org在线运营者未确认
试用过了吗? 点一下就好,无需登录。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证MIT-licensed astrology calculation engine: positions, houses, points and timing techniques, with receipts and a public conformance suite
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Pure TypeScript astrology calculations for browsers and Node.js. The package
computes tropical planetary positions, natal charts, transit snapshots,
synastry, secondary progressions, Moon phase, and Saturn-return seasons, and,
in separate entries, one calculation API over frames, centers and
corrections with bounds and receipts, birth-time windows, house positions and
cusp speeds, rise, set and transit times and planetary hours, returns and
relationship charts, Hellenistic timing techniques, and sidereal positions
and Jyotish techniques. Its calculations are synchronous; only loading a zone's
history before 1970 (prepareLocalTime) and the GeoNames client, both in
@zodiacs/engine/geo, are asynchronous. It is ESM-only, has no import-time
side effects, and performs no network request from its core entry point. Its
one runtime side effect is the ΔT it installs in astronomy-engine (see Time
below).
1.1.0-rc.1, a candidate for 1.1.0, not published: natal receipts name
their conventions as ids of the Zodiacs conventions registry, which a new
entry, @zodiacs/engine/conventions, gives (see Draft natal receipts;
CHANGELOG.md). The package follows Semantic Versioning:
docs/versioning.md says what is public, how a number
may change, which records stay readable and how something public is
deprecated, and api/ holds each public entry point's declarations. npm
carries 1.0.0, the first stable release, under latest: npm install @zodiacs/engine installs it, without ./conventions and writing no ids.
This candidate is not published there. Install the exact candidate tarball,
artifacts/zodiacs-engine-1.1.0-rc.1.tgz in the repository, retaining its
SHA-256 receipt:
npm install ./zodiacs-engine-1.1.0-rc.1.tgz
The Release workflow publishes the archive carried in artifacts/ as it is,
with provenance (CONTRIBUTING.md, Releases). Before 1.0.0 npm carried
0.1.1-rc.14 to 0.1.1-rc.16. 0.1.1-rc.17, 1.0.0-rc.1 and 1.0.0-rc.2 were never
published, so a caller moving from npm reads their CHANGELOG entries,
1.0.0-rc.1's Migration first, and a caller on 0.1.1-rc.15, npm's latest
until 1.0.0, reads 0.1.1-rc.16's too.
The package runs in browsers through a bundler, and in Node.js 20.19.0 or a
later 20.x, or 22.7.0 or later ("engines": { "node": "^20.19.0 || >=22.7.0" }).
Node.js 20 reached its end of life on 2026-04-30: the package is still tested
there, but a 1.x minor release may drop it (docs/versioning.md, Runtimes).
Its dependency, astronomy-engine 2.1.19, ships ES modules in a package that does
not declare "type": "module". Earlier Node versions load that file as
CommonJS, so a plain Node import of this package fails there with "Named export
'Body' not found"; it did on 18.20.8, 20.18.3, 21.7.3 and 22.6.0. Node 22.7.0
loads it with a harmless MODULE_TYPELESS_PACKAGE_JSON warning, which 22.22.2
no longer prints. Bundlers resolve the dependency's import condition
themselves and are not affected.
From a source checkout, run npm ci and npm run build, then
npm pack --ignore-scripts. Test the packed file in a clean consumer using
npm run consumer:smoke -- /absolute/path/to/zodiacs-engine-1.1.0-rc.1.tgz.
The smoke check
downloads the artifact's public dependencies and TypeScript 5.9.3; its output
records the artifact hash and runtime, and it removes its temporary consumer
directory, which must have no node_modules above it (set TMPDIR if
needed). A packed candidate is not a published release.
artifacts/archives.json records every carried archive: its digest, size, file
count and the commit it was packed from. On the history it is given, CI checks
from git objects alone, never the working tree, that in every commit:
artifacts/, where present, is a real directory holding only archives,
receipts, the manifest and its README, as regular files, and no two of its
names, nor another top-level name and artifacts, differ only in case; each
archive and receipt holds only its recorded bytes, the one exception, pinned in
the check, being rc.11's first packing in commit 00bdae7; and the manifest
only ever gains entries. Every recorded version, and package.json's at HEAD,
must be a strict semantic version, with no v prefix and no build metadata,
and no two carried versions may be equal as npm compares them. It also checks
that nothing committed under artifacts/ has been removed; that each archive's
packed package.json, README, CHANGELOG and licence files match its source
commit's byte for byte, and that it was committed by that commit or a child of
it; and, once the current version's archive is carried, that a clean worktree
of HEAD rebuilds it byte for byte (--rebuild-all also rebuilds every archive
from its source commit). Each rebuild installs that commit's locked
dependencies afresh with npm ci and takes nothing from the checkout's
node_modules. It cannot detect history rewritten before CI sees it, and it
needs merge commits: a squash or rebase merge drops the source commits it
checks against, and the check then fails.
1.0.0-rc.1 settled the API for 1.0.0. Before the promise was made, three
reviews read every public declaration of the twelve entry points; it acted on
what they found, recorded the result in api/, which CI checks, and lists
every breaking change, deprecation and experimental part in CHANGELOG.md with
what a caller does about it. @zodiacs/engine/calc now computes a caller's
ayanamsa carried by precession from any epoch in EPHEMERIS_SPAN, where rc.17
refused one outside 1800 to 2200. Where both compute a value, it is rc.17's,
to the bit, in a comparison of 6,411 calls across the twelve entry points.
rc.17 added the sidereal zodiac to @zodiacs/engine/calc: its four
functions take an ayanamsa of @zodiacs/engine/vedic, or a caller's own,
and, with every other default, give the Vedic entry's sidereal longitudes to
the bit; their bounds add the ayanamsa's (see Uniform calculation API). rc.16
brought five opt-in entry points onto rc.15:
@zodiacs/engine/calc, one calculation API over eight frames, four centers
and three corrections, with speeds, bounds and receipts; /window, birth-time
window partitions; /techniques, returns, composite and Davison charts, the
void-of-course Moon, aspect patterns, dignities and Moon signs; /houses,
house positions of bodies with latitude, co-ascendants and cusp speeds; and
/sky, rise, set, transit and planetary hours. @zodiacs/engine/timing
gained planetary returns. The root imports none of them. rc.16 also made the
nutation the full IAU 2000B series, which the engine evaluates itself (see
Nutation): every longitude moved from rc.15's by the change in Δψ, up to
0.2701″ from 1800 to 2200, and the angles and cusps by the change in the
sidereal time and the true obliquity, up to 0.8003″ over 9,697 synthetic
charts at places up to 60.17° N, and more toward the polar circle. Receipts
gained a conventions set that names the nutation; rc.15's receipts stay
readable. As in rc.15, a chart's instant is read on a time basis: from 1972
to 2027-10-02 as UTC, with TT from the leap seconds and UT1 from IERS
UT1 − UTC, and otherwise as UT1 with the ΔT model, or on UT1 or TT when
timeScale says so (see Time). The ephemeris is still astronomy-engine
2.1.19. See CHANGELOG.md for the release history and
docs/evidence/1.1.0-rc.1-20261026/ for this candidate's checks (1.0.0's are
in docs/evidence/1.0.0-20261011/, 1.0.0-rc.2's in
docs/evidence/1.0.0-rc.2-20261006/, 1.0.0-rc.1's in
docs/evidence/1.0.0-rc.1-20261006/, rc.17's in
docs/evidence/rc17-20261005/). Site adoption is reviewed separately.
import { natalChart } from "@zodiacs/engine";
const chart = natalChart({
utc: "1990-06-15T12:30:00Z",
latitude: 40.7128,
longitude: -74.006,
houseSystem: "whole"
});
console.log(chart.bodies, chart.houses);
utc must be a resolved instant. If a user enters a local wall time, use the
optional geo entry point so daylight-saving and historical timezone rules are
handled before the chart is computed:
import { natalChart } from "@zodiacs/engine";
import { resolveBirth } from "@zodiacs/engine/geo";
const birth = resolveBirth({
date: "1990-06-15",
time: "08:30",
timeZone: "America/New_York",
latitude: 40.7128,
longitude: -74.006,
houseSystem: "placidus"
});
const chart = natalChart(birth);
For a date before 1970, first await prepareLocalTime(date, timeZone), also
from @zodiacs/engine/geo: it loads the zone's history (see Time).
import { natalChart, synastry } from "@zodiacs/engine";
const a = natalChart({ utc: "1990-06-15T12:30:00Z" });
const b = natalChart({ utc: "1992-11-03T07:15:00Z" });
const compatibility = synastry(a, b);
console.log(compatibility.top);
console.log(compatibility.elements);
import { natalChart, transits } from "@zodiacs/engine";
const natal = natalChart({ utc: "1990-06-15T12:30:00Z" });
const today = transits(natal, new Date());
for (const aspect of today.aspects) {
console.log(aspect.a, aspect.type, aspect.b, aspect.orb);
}
positions(date) returns the Sun, Moon, eight planets, and true Moon nodes.natalChart(birth) adds natal aspects and, when coordinates are present,
angles and houses in any of thirteen systems (see House systems).chartPoints(natal) returns the mean node, Black Moon Lilith and, with a
birth time and place, the Vertex, the East Point, the chart's sect and seven
lots (see Points).findConfiguredAspects(bodies, policy) calculates aspects under an explicit
policy from createAspectPolicy, including minor and custom angles.chartDeclinations(natal) derives right ascension, declination, parallel
aspects and out-of-bounds flags on the chart's clock.progressedInstant(birthUtc, target) maps each elapsed tropical year of
life to one day after birth.progressedBodies(birthUtc, target) returns the twelve ordinary position
rows at that instant (see Secondary progressions).transits(natal, date) returns a sky snapshot and moving-to-natal aspects.synastry(a, b) returns inter-chart aspects and element/modality balances.moonPhase(date) returns elongation, illuminated fraction, and phase name.saturnReturn(birth) returns exact-pass seasons through roughly age 92.findLongitudeCrossings(body, longitude, from, to) returns the instants a
body crosses a longitude, and searchLongitudeCrossings does the same under
a sample budget. @zodiacs/engine/crossings provides the same solver for a
longitude function of your own, without the ephemeris.@zodiacs/engine/geo provides IANA local-time resolution, Julian calendar
dates and a client for a separately hosted, sharded GeoNames index.@zodiacs/engine/techniques provides solar and lunar returns, composite and
Davison charts, the void-of-course Moon, aspect patterns, essential
dignities and the Moon signs possible over a date (see docs/techniques.md).@zodiacs/engine/timing provides profections, firdaria, zodiacal releasing
and solar arc directions (see docs/timing-hellenistic.md), and planetary
returns (see docs/houses.md).@zodiacs/engine/houses provides the house position of a body with ecliptic
latitude in each house system, the co-ascendants and the polar ascendant,
and the speeds of the cusps and the angles (see docs/houses.md). It
imports no ephemeris and no module of the root entry.@zodiacs/engine/calc offers calc, houses, events and chart with one
vocabulary: instants as ISO strings, Dates or { jd, scale: "utc" | "ut1" | "tt" }
on the engine's time basis, as positions() reads them;
eight frames (the ecliptic or the equator; true or mean of date, J2000.0 or
the ICRS); geocentric, heliocentric, barycentric and topocentric centers;
apparent, astrometric or geometric positions with distances and speeds; and
the tropical or the sidereal zodiac, with any ayanamsa of
@zodiacs/engine/vedic or a caller's own (Swiss Ephemeris's SE_SIDM_USER).
Results carry bounds and a receipt of convention ids. A measured bound is the
largest difference from JPL Horizons on 32 instants from 1802 to 2188: a
sample maximum, not a limit. In the sidereal zodiac a bound adds the
ayanamsa's: the largest difference of its mean, and of its rate, from ERFA's
construction of the same definition, over every year from 1800 to 2199. An
estimated bound names what it rests on. What this version does not compute
comes back as a typed refusal, such as gravitational deflection, or an
instant whose UT1 or TT is outside 1800 to 2200: there calc has no
comparison to take a bound from, while the root entry's functions
still compute such an instant and flag it outside-reference-span. With
every default, calc({ body, time }) is the position positions() gives, to
the bit. The root entry loads none of it. The reference, with the Swiss Ephemeris flag
mapping and the measured accuracy, is
docs/calc.md.
Returned longitudes use degrees in [0, 360) and positions include sign and degree
annotations. Charts use the tropical ecliptic of date. Planetary positions are
geocentric and corrected for light time and aberration, but not for the Sun's
gravitational deflection; the Moon's series carries neither correction. The
chart functions do not calculate topocentric parallax; @zodiacs/engine/calc
gives topocentric positions (center: { topocentric }), and
@zodiacs/engine/sky finds rise and set for a topocentric observer.
Positions have been compared with an independent ephemeris from 1800-01-01T00:00Z
up to 2200-01-01T00:00Z, exported as REFERENCE_SPAN. A chart outside that span
is still computed, and carries the outside-reference-span flag. No instant
outside EPHEMERIS_SPAN, the years 1 to 3998 that astronomy-engine tabulates,
is computed at all (see Resolved instant inputs).
import {
progressedInstant, progressedBodies, PROGRESSION_DAYS_PER_YEAR
} from "@zodiacs/engine";
const birthUtc = "1990-06-15T12:30:00Z";
const target = "2026-09-28T00:00:00Z";
console.log(progressedInstant(birthUtc, target));
console.log(progressedBodies(birthUtc, target));
The fixed convention is one 365.2422-day tropical year of elapsed life
for one 86,400,000-millisecond day after birth. PROGRESSION_DAYS_PER_YEAR
exports that constant. Both are counted in UTC milliseconds as JavaScript
Date counts them, 86,400,000 to the day with no leap seconds; they are not
ephemeris (TT) days. This uses elapsed instants, not calendar anniversaries,
local-time days or daylight-saving adjustments. Both arguments accept a valid
Date, finite epoch-millisecond timestamp, or ISO calendar date/date-time.
Date-only strings mean UTC midnight; date-times require an explicit offset.
Invalid dates and ambiguous local date-times throw RangeError. Resolve local
birth times with the optional geo entry first.
Targets before birth are accepted and map backwards with the same signed
formula. That signed extension is not a converse progression, which takes a
target after birth to an instant before it; no converse technique is offered.
Results preserve the site's existing floating-point operation order
and JavaScript Date truncation to integer milliseconds. Neither argument is
mutated, and the returned Date and position rows are newly allocated.
progressedBodies returns precisely positions(progressedInstant(...)):
Sun, Moon, eight planets and the true north/south lunar nodes. Speed is the
ephemeris longitude rate at the progressed instant, in degrees per day: a
central difference over ±0.001 day (±86.4 s), and over ±0.25 day for the true
nodes, as in every position row. One progressed day stands for one tropical
year of life, so the same number is the progressed motion in degrees per
365.2422-day year of elapsed life. It is not a rate per lived day. No
progressed angles, houses, extra chart points, solar-arc direction,
progressed aspect policy or natal-receipt extension is implied.
Positions retain the existing ephemeris and its accuracy limits. Reference
coverage applies to the progressed instant, not to the target age or date;
call outsideReferenceSpan(progressedInstant(...)) to check it. These APIs
return an instant or rows, without chart coverage flags. Acceptance of a valid
Date does not establish physical accuracy for that date. Validation includes a
constructed mapping to the existing JPL longitude fixture and a rounded
published date-mapping example; neither establishes predictive validity.
The year convention and published mapping are cited to Juan Estadella,
Predictive Astrology, 3rd ed., pp. 84–85
(PDF SHA-256 bf52656b367ad7d1415a7b021a0a0db3a609bf35c7a40053ff0622e7a3325622).
The book's own rounding allows up to 4.73 s, hence the 5 s comparison; from the
19.677 h birth time it actually used, the mapping lands within 50 ms of its
unrounded result. See docs/evidence/rc12-20260928/sources.md.
import { createAspectPolicy, findConfiguredAspects } from "@zodiacs/engine";
const policy = createAspectPolicy({
aspects: [
{ type: "conjunction", orb: 8, luminaryOrb: 10 },
{ type: "quincunx", orb: { applying: 2, separating: 1, stationary: 0.5 } },
{ type: "quintile", orb: 1 }
],
bodyOrbs: { Moon: 5, Pluto: 1 }
});
const analysis = findConfiguredAspects(chart.bodies, policy);
console.log(analysis.policy, analysis.aspects);
createAspectPolicy() returns an immutable snapshot of the five existing
major-aspect defaults. CONFIGURED_ASPECT_ANGLES also names semisextile (30°),
semisquare (45°), quintile (72°), sesquiquadrate (135°), biquintile (144°) and
quincunx (150°). Choose their orbs explicitly; custom identifiers require an
explicit angle. Named angles cannot be redefined.
For each pair, the rule's orb applies, replaced by its luminaryOrb when
either body is Sun or Moon. Each selected body's bodyOrbs value is an upper
bound: the smallest of the rule allowance and both body caps wins. A number
sets all three motion limits; an object sets each separately. Boundaries are
inclusive. The closest eligible aspect wins, with definition order breaking
ties. Results sort by exact orb, then input-pair order.
bodies explicitly selects identifiers; it defaults to Sun, Moon and the
eight planets. Nodes or other points require selection. Labels, here and in
the declination analysis, are exact and case-sensitive strings of 1 to 80
UTF-16 code units (a character outside the Basic Multilingual Plane counts
two). String.prototype.trim must leave a label unchanged, which rules out
leading or trailing whitespace, including U+3000 and U+FEFF, and a label may
not contain a C0 control character (U+0000–U+001F) or DEL (U+007F). C1
controls (U+0080–U+009F), zero-width characters such as U+200B, and lone
surrogates are not rejected. Other labels throw RangeError.
Positions require finite longitude in [0,360) and a finite longitude speed
in degrees/day
on the same time basis. Missing or null speed is rejected, including on
unselected rows. A point whose speed is unknown must not be assigned zero to
make it pass. Equal speeds, or relative speed below the policy threshold,
are stationary; an exact moving aspect is separating. The one-sided motion
at coincident and antipodal longitudes is defined in the returned policy.
Policies and results are deeply frozen. Pass the factory's policy object to
findConfiguredAspects; a JSON-deserialized copy must have its input fields
revalidated through the factory. Its schema and conventions describe the
calculation; they are not an authenticated receipt. This analysis does not
replace chart.aspects, or configure synastry or transits.
Every longitude, speed, angle, orb and threshold is taken as the exact value
of its binary64 double. The signed separation a − b is formed exactly, as an
unrounded sum of doubles, and folded into (−180°, 180°] by exactly 360°. The
orb |separation − angle|, its inclusive comparison with the limit, the choice
of the closest rule, the order of results, and the motion (the signs of the
separation, of the deviation from the angle and of the relative speed, and the
stationary threshold) are all decided on those exact values. There is no
epsilon or widened tolerance. Only the reported orb is rounded, once, to the
nearest double with ties to even, so a reported orb never exceeds its
maximumOrb. The policy records this as
conventions.arithmetic: "exact-binary64;reported-orb-rounded-half-even".
Binary values are not decimals. At the default 7° square, 97.00000000000001 is not rounded into eligibility. 188.86 − 98.86 is exactly 90 + 2⁻⁴⁶, so a zero-orb square does not match; 7.6999999999999895 and 359.7 are 8 + 2⁻⁵⁰ apart, just outside the default 8° conjunction. 6.3 and 314 are exactly 45° plus the double 7.3 apart (6.3 carries 7.3's binary error), so a 7.3° semisquare orb includes them. Where a decimal boundary matters, use values that are exact in binary or allow for the difference in the orb.
The historical helper behind chart.aspects, transits, synastry and the
natal receipt keeps its floating-point arithmetic, so its orbs and its
boundary decisions can differ from the configured API's at roundoff scale.
import { chartDeclinations, findDeclinationAspects } from "@zodiacs/engine";
const equatorial = chartDeclinations(chart);
console.log(equatorial.utc, equatorial.deltaT, equatorial.trueObliquity);
console.log(equatorial.rows); // lon, lat, ra, dec, outOfBounds, boundMarginArcsec
const tightParallels = findDeclinationAspects(
chart.bodies, equatorial.trueObliquity, { orb: 0.5, luminaryOrb: 1 }
);
chartDeclinations accepts a birth input or an existing chart. It rotates
the full ecliptic longitude and latitude into the true equator and equinox
of date, using the chart instant and the same pinned or model ΔT. Right
ascension is in degrees, not hours, and is null where the horizontal
unit-vector magnitude is no greater than RA_POLE_TOLERANCE; raDefined
then is false. Declination is north-positive in [-90,90].
Out-of-bounds means strictly abs(dec) > trueObliquity, without an
uncertainty allowance. Each row also carries boundMarginArcsec, the signed
margin (abs(dec) − trueObliquity) × 3600: positive beyond the bound, negative
inside it. For every row but the exempt Sun below, outOfBounds is exactly
boundMarginArcsec > 0. In chartDeclinations, trueObliquity is the
engine's true obliquity of date at the chart instant on the chart's clock: the
IAU 2006 mean obliquity plus the IAU 2000B nutation in obliquity, all 77 terms
(see Nutation). It equals ERFA's obl06 plus nut00b within the tests'
tolerances (1e-9″ and 1e-10″), and at 20,000 instants from 1850 to 2150 it was
within 1.63 mas of obl06 plus nut06a (IAU 2000A), where astronomy-engine's
five-term value, used until this change, was up to 85.6 mas from both. The
pure functions use the obliquity they are given.
The flag describes the ephemeris's position, and it agrees with the real sky
only where the margin exceeds the ephemeris's error in declination. Against
JPL's DE440s at those 20,000 instants, the largest declination errors were,
rounded up (docs/evidence/nutation-2026-09-29/results/declination-truth.json):
| Body | Largest error | Body | Largest error |
|---|---|---|---|
| Sun | 2.7″ | Jupiter | 16.1″ |
| Moon | 3.4″ | Saturn | 21.6″ |
| Mercury | 12.2″ | Uranus | 19.3″ |
| Venus | 14.8″ | Neptune | 15.5″ |
| Mars | 14.6″ | Pluto | 4.6″ |
These are sample maxima, not bounds, and they grow outside REFERENCE_SPAN.
Within them the flag can be wrong either way: at 2022-10-22T08:11:10.756Z the
engine puts Mars 1.57″ inside the bound, where DE440s has it 1.05″ beyond.
Treat a flag as settled only when abs(boundMarginArcsec) exceeds the body's
figure.
The Sun's exemption is a convention. At a solstice the size of the Sun's
declination differs from the true obliquity by exactly its ecliptic latitude,
which stays within about 1.2″ of zero: computed with ERFA (IAU 2006/2000A), the
real Sun is beyond the bound at 402 of the 800 solstices from 1800 to 2199, by
up to 1.09″. The engine's solar declination is off by more than that, up to
2.7″ in the comparison above, and its solar latitude drifts from −1.1″ on
average in the 1800s to +1.1″ in the 2100s, so its margin at a solstice has the
real sign at only 404 of those 800. Far from J2000 the drift grows to tens of
arcseconds, −68.3″ at the June solstice of year 2 and +25.8″ at that of 3902,
while the real Sun's latitude stays within about 1.2″. So chartDeclinations
never flags the chart's own Sun as out of bounds, at any latitude; its margin
is still reported, and says nothing about the real Sun. (It is the Sun of the
chart given, which is not recomputed.) declinationsForBodies exempts a
supplied row labelled exactly Sun only while its ecliptic latitude is within
SUN_BOUND_LATITUDE (0.001°, 3.6″); a Sun row with a larger latitude, such
as a synthetic input, and every other row keep the strict rule.
Every supplied body is eligible, including nodes; filter the input for a
smaller set. Parallel/contraparallel matches choose the smaller of
abs(decA-decB) and abs(decA+decB), with parallel winning an exact tie.
Orbs are inclusive, defaulting to 1° or 1.5° when Sun or Moon is involved.
As with configured aspects, each declination double is taken as exact: the
choice, the inclusive orb test and the order are decided without rounding, and
only the reported orb is rounded, once. The longitude separation is the
short way round between the two supplied longitudes, reduced exactly modulo
360 whatever their size, and rounded once: −0.1 and 0.2 are
0.30000000000000004 apart, the exact sum of the doubles 0.1 and 0.2, rounded.
13.3 and 12.3 are exactly 1° apart and match at orb 1; 8.3 and 7.3
(1 + 2⁻⁵⁰ apart), and 1.1 and 0.1 (1 + 3·2⁻⁵⁵), do not. Body labels follow
the configured-aspect rule. eclipticToEquatorial and declinationsForBodies
expose the same geometry with an explicitly supplied obliquity.
These are derived coordinates of the existing ephemeris, with its existing corrections and limits. They do not establish the programme's 0.01″ physical declination target against Swiss Ephemeris. The current natal receipt does not include this analysis; the result states that scope explicitly.
houseSystem takes one of thirteen systems. Each is the definition Swiss
Ephemeris uses, and every one agrees with Swiss's swe_houses_armc to within
0.0001″ given the same sidereal time, latitude and obliquity (Placidus, which
iterates, to 0.01″). End to end, from an instant read as UT1 as Swiss reads
it, every system was within 0.055″ of Swiss's swe_houses_ex from 1850 to
2049, on a ladder of latitudes from 55° to 66.6° and on 3,000 draws within 66°
of the equator (docs/evidence/nutation-2026-09-29/results/ladder.json);
outside those years Swiss uses a long-term sidereal time of its own.
houseSystem | System | Cusps |
|---|---|---|
"whole" | Whole sign | Each sign is a house, the first the ascendant's. The default. |
"placidus" | Placidus | Semi-arcs of each degree in thirds. |
"koch" | Koch | Ascendants at thirds of the midheaven degree's semi-arc. |
"porphyry" | Porphyry | Each quadrant between the angles in three equal arcs of longitude. |
"regiomontanus" | Regiomontanus | Circles through the horizon's north and south points, every 30° of the equator. |
"campanus" | Campanus | The same circles, every 30° of the prime vertical. |
"topocentric" | Topocentric (Polich–Page) | Regiomontanus's ascensions, with pole heights at a third and two thirds of the latitude's tangent. |
"alcabitius" | Alcabitius | The ascendant's semi-arcs in thirds on the equator, along hour circles. |
"equal" | Equal | 30° each from the ascendant. |
"equal-mc" | Equal from the midheaven | 30° each, the 10th from the midheaven; the same at every latitude. |
"vehlow" | Vehlow | 30° each, with the ascendant in the middle of the first. |
"meridian" | Meridian (axial rotation) | Right ascensions every 30° from the midheaven's; the 1st cusp is the equatorial ascendant. |
"morinus" | Morinus | The equator every 30° from the midheaven's right ascension, carried to the ecliptic through its poles. |
Placidus and Koch are undefined in polar regions, where |latitude| ≥ 90° − ε,
with ε the true obliquity of date (about 66.56° today). There the engine falls
back to whole-sign houses, exported as POLAR_FALLBACK (and as
PLACIDUS_POLAR_FALLBACK, deprecated from 1.0, since Koch falls back too),
and adds polar-fallback to the chart flags. Swiss
Ephemeris falls back to Porphyry instead. Every other system is defined at
every latitude where the angles are. Inside the polar circle, where the
ascendant is taken on the eastern half of the horizon, Regiomontanus, Campanus
and Topocentric cusps turn with it, so their 10th cusp is then the lower
meridian, as in Swiss Ephemeris. When the birth time is unknown, pass a
conventional UTC instant with timeKnown: false; angles and houses remain
absent and the chart carries the no-time flag.
@zodiacs/engine/houses places a body with ecliptic latitude in any of these
systems, as Swiss Ephemeris's swe_house_pos defines the position, and gives
the co-ascendants, the polar ascendant and the speeds of the cusps and angles
(docs/houses.md).
chartPoints(natal) takes a birth or a chart and returns { sect, points }.
Each point has a longitude, a latitude, a sign and a degree, like a body.
| Point | Definition |
|---|---|
| Mean Node, Mean South Node | The Moon's mean ascending node Ω, and its opposite. |
| Black Moon Lilith | The mean lunar apogee: the point of the mean orbit 180° from the mean perigee, with the orbit's latitude. |
| Vertex | Where the prime vertical meets the ecliptic in the west. |
| East Point | The equatorial ascendant: the ecliptic point at right ascension RAMC + 90°. |
| Lots of Fortune, Spirit, Eros, Necessity, Courage, Victory and Nemesis | Paulus Alexandrinus's seven lots, reversed by night. |
The mean node and Black Moon Lilith come from the Moon's mean elements: the
IERS Conventions' fundamental arguments (Simon et al. 1994), with a mean
inclination of 5.1453964°. The nutation in longitude puts them on the true
equinox of date, like every other longitude here. At 2,000 instants from 1800
to 2199 they were within 0.4503″ and 0.4887″ of Swiss Ephemeris's
SE_MEAN_NODE and SE_MEAN_APOG
(docs/evidence/nutation-2026-09-29/results/swiss.json). Both carry a speed in
degrees per day.
Every chart gets those three. The Vertex, the East Point, the sect and the lots need a birth time and place.
swe_houses_armc to within 0.00001″.asc + moon − sun.antiscion, contraAntiscion and midpoint work on any two longitudes.
meanNodeLongitude, meanApogee, lunarMeanArguments, hellenisticLots and
sectOf expose the calculations underneath. The first three, with
MEAN_LUNAR_INCLINATION, are experimental from 1.0
(docs/versioning.md): a minor release may change them,
and chartPoints and @zodiacs/engine/calc give the points themselves. The osculating ("true") Lilith is
not offered: from astronomy-engine's lunar series it would be several
arcminutes from Swiss's.
Public birth inputs accept all six ChartFlag values. Supply an array with at
most 64 entries; entries must be known string values in ordinary data slots.
Repeated values collapse in first-occurrence order. Unknown strings, sparse
slots, accessor slots, non-array iterables and simultaneous dst-gap/dst-fold
claims reject with a RangeError that does not include supplied flag values.
dst-gap, dst-fold and lmt remain caller assertions: a UTC instant alone
cannot verify a historical local-time resolution. The geo resolver sets
dst-gap or dst-fold for a skipped or repeated wall time of any cause (the
names are kept; its jump.cause gives the cause) and lmt when a local mean
time clock read it. no-time,
polar-fallback and outside-reference-span may be echoed for compatibility,
but must agree with the calculation. Set timeKnown: false for unknown time; a flag never overrides that
setting. A fallback assertion requires the actual requested Placidus calculation
to produce whole-sign houses, which it does only inside the polar circle.
natalChart stores only distinct time-resolution assertions in chart.input.flags
and derives result flags once. This is canonical semantic input, not a lossless
record of the submitted flag array. Correct derived echoes therefore work with
the existing draft receipt creator and replay; the receipt schema is unchanged.
When transits, synastry or saturnReturn receive a precomputed Chart, they
check flag consistency with its supplied time/settings and house/angle presence.
Contradictory or missing result claims reject. They do not recompute or verify
the supplied astronomical result. An already canonical Chart retains object
identity; compatible duplicate/derived echoes produce a shallow metadata copy,
preserving numerical arrays and engine version. These checks happen at call
time and do not freeze caller-owned objects.
Ordinary Saturn-return inputs require no extra natal calculation. A raw birth
input that explicitly asserts polar-fallback requires one natal calculation to
check that assertion before the return scan. A supplied Chart instead receives
the consistency check above, without natal recomputation. Public birth and
resolveBirth settings are validated once and the captured scalar values are
used for calculation/resolution. Explicit null settings or local time are
invalid. These boundaries do not sandbox same-realm getters, proxies or other
caller-supplied executable code. The unsupported /internal computation API is
unchanged.
The shared engine selects the eastern horizon intersection before assembling houses, including in either polar hemisphere. At exact geographic poles no point physically rises; at ecliptic/horizon coincidence an ascendant is not unique. Those degenerate configurations are outside the verified angle scope. Near tangencies the selected axis can change by 180 degrees. Placidus uses a bounded iteration and, where it does not settle, bisection on each cusp's bracket, which cannot fail outside the polar circle; it never returns the last unconverged iterate. Near the polar limit a cusp carries the rounding of asin near ±1: up to 6.5e-7° one unit in the last place below it.
See CHANGELOG.md for each release's changes. Reference coverage and known limits are recorded in the site platform evidence ledger. The date parser's representable range is not a claim of astronomical accuracy across that range. Reference cases are finite; broader numerical scope review remains a release gate.
findLongitudeCrossings(body, longitude, from, to, stepDays = 5) returns every
instant in the half-open window (from, to] when body is exactly at
longitude, in time order, each marked retrograde when the body was moving
backward through it. A root exactly at from belongs to the window that ends
there and is not returned; a root exactly at to is. There is no sample
budget, and the call does not throw for the size of a search: its work grows
with (to − from) / step.
searchLongitudeCrossings(body, longitude, from, to, { stepDays, maxSamples })
makes the same search under a budget of ephemeris evaluations, root
refinements included. It returns { status: "complete", crossings, samples },
or { status: "refused", reason: "sample-budget", samples, maxSamples, crossings: [] }. It refuses before any sampling when the coarse scan alone
needs more than maxSamples, and otherwise when a refinement would pass the
budget. It never returns part of a result and never throws for the budget.
Without maxSamples there is no limit.
Both run the one solver, exported by @zodiacs/engine/crossings as
findLongitudeCrossingsWith(longitudeAt, body, longitude, from, to, stepDays)
and searchLongitudeCrossingsWith(longitudeAt, body, longitude, from, to, options). It takes the longitude function as its first argument and imports
no ephemeris, so that entry point carries none; the root entry point exports
the same two functions.
The solver samples from, every stepDays after it and to, and bisects each
sign change of the offset from the target 24 times, to the step divided by
2^24: 25.7 ms at 5 days, 1.3 ms at a quarter day. A sample exactly on the
target is returned once, at that sample, with the direction of the sample
before it; a sampled plateau on the target is returned where it begins.
Offsets of 90° or more on either side of a step are the far side of the circle,
not a crossing.
A fixed step alone loses both crossings when a station falls between two samples just past the target: at 5 days, a Saturn station within 0.0103° of it, or a Jupiter station within 0.0205°. Wherever the sampled motion turns without the offset changing sign, and the turning sample is within reach of the local curvature, the solver finds the extremum by golden-section search. It bisects both crossings when the extremum passes the target, and returns one, as direct, when it only touches. A turn in the first or last step is found by a probe one minute inside the window, and no sample falls outside [from, to]. Crossings less than a second apart are returned once. The solver assumes smooth motion with at most one station in two steps. That holds for the Sun, the Moon and the planets at the steps the site scans with, from a quarter day for the Moon to 5 days for Saturn to Pluto, but not for the wobbling true node. It is tested on synthetic and real stations, not proven complete.
Invalid input throws RangeError before any sampling: an invalid Date,
from after to, a non-finite longitude, a step that is not positive or is
shorter than a millisecond, or a maxSamples that is not a positive integer or
Infinity. A non-finite longitude from the ephemeris throws RangeError too.
import { birthWindow } from "@zodiacs/engine/window";
const result = birthWindow({
start: "1990-06-15T12:20:00Z",
end: "1990-06-15T12:40:00Z",
latitude: 40.7128,
longitude: -74.006,
houseSystem: "placidus",
rounding: { recorded: "1990-06-15T12:30:00Z", minutes: 5 }
});
for (const cell of result.cells) console.log(cell.start, cell.share, cell.roundedShare, cell.features.ascendant);
for (const change of result.switches) console.log(change.at, change.changes);
birthWindow divides a window of possible birth instants into cells within
which every discrete feature of the chart is constant: each body's sign and
house, the signs of the ascendant and the midheaven, the aspects in orb and,
for Placidus and Koch, whether the houses fall back to whole signs. The
features are natalChart's: houses are houseOf on the chart's cusps, and
aspects are its five major aspects with their orbs. Each switch gives the
first millisecond of its new cell and every change at it, from and to. Each
cell gives its share of the window under a uniform prior and, when rounding
is given, under that rounding model. Nothing else is partitioned: two instants
in one cell can differ in natalChart's retrograde flags, positions within a
sign, speeds, orbs and whether they apply, the degrees of angles and cusps,
declinations and chart points.
The window is start to end, excluding end, at most 48 hours; or at
and minutes, for minutes either side of at; or only rounding, whose
unit is then the window. It must lie inside REFERENCE_SPAN (1800-01-01 to
2200-01-01, UTC), where the search's bounds were scanned. rounding: { recorded, minutes, mode } says the recorded time was rounded to minutes,
to the nearest (the default) or "down"; the true instant is taken as
uniform over that unit, which must lie inside the window. Resolve local times
first, for example with resolveLocalToUtc from @zodiacs/engine/geo.
Every value is the engine's own at a millisecond, as natalChart computes it
on UTC and the engine's time basis (not a pinned deltaT). Where the search's
bounds hold, each cell holds
natalChart's features at every millisecond in it, and at each switch
natalChart's value at the millisecond before at differs from its value at
at exactly as listed, except inside an interval listed in unresolved.
The search halves the window until each feature is settled, dropping an
interval for a feature when enclosures of the quantities it depends on keep
clear of every threshold and the feature agrees at both ends; single
milliseconds are compared directly. The enclosures rest on bounds scanned
from 1800 to 2200: WINDOW_RATE_BOUNDS (about twice each body's largest
rate), the obliquity's rate, and the true node's millisecond jitter, J =
5e-5·(1 + |T|)° with T in centuries from 2000, at least 3.01 times every
departure found at 3,000 consecutive milliseconds every five days
(docs/evidence/birth-window/rc16/node-jitter.json). They assume TT and
UT1 run on continuously. The time basis steps at the ΔT model's seam,
1940-12-31T18:00:00Z, where ΔT steps from
24.834 s to 24.820 s; at 1972-01-01, where the leap seconds begin; at each
leap second, where TT and UT1 step by a second; and where the IERS UT1 table
ends, 2027-10-02. Each sample installs its own ΔT, so every position and
angle steps there together: the search finds each step inside the window
from the engine's own arithmetic, splits at its millisecond and compares
both sides. Within 1e-8° of the Placidus
or Koch polar limit, near the RAMCs where their cusps carry the rounding of
asin near ±1, the house system and the houses are compared at every
millisecond. A failed check of an enclosure adds bound-exceeded, and
completeness is then not established; a failure that does not show in an
interval's end-to-end change goes unseen. The method in full is in
docs/evidence/birth-window/RESULTS.md.
Near a sign boundary the true node's jitter makes its sign change back and
forth between milliseconds, for about 4J/|rate| days around an ingress: 1.5
minutes at the fastest from 1800 to 2200, 4.5 at the median, 2.3 hours at the
slowest. Every change is reported where the budget of two million evaluated
instants allows (299 at the ingress of March 2028,
docs/evidence/birth-window/rc16/node-flicker.json). Where the node
would need more, the stretch is left unresolved, found before any of the
budget is spent on it: the nodes' signs, and their houses where the houses are
whole signs, are null there, the interval is listed in unresolved, and the
result carries node-unresolved; a change to or from null is its edge, not a
crossing. 24 of the 294 ingresses from 1800 to 2200 flicker for longer than
the budget, in 1810, 1842 (two), 1848, 1865 (two), 1882 (two), 1946 (two),
1981 (two), 2007, 2032 (two), 2049 (two), 2071, 2090 (four), 2113 and 2127
(node-ingresses.json). A true node from the Moon's analytic velocity,
instead of astronomy-engine's 1.728 s difference, would be smooth and leave
nothing unresolved; it changes natalChart's node in its last digits and is
left for a later release.
Results carry verification: "sampled at one-second resolution": checked
against natalChart at every whole second, not proven. On 1,000 preregistered
random windows (1800 to 2200, all thirteen systems, two thirds at 60° of
latitude or more), all 24,188 sampled transitions were matched, none missed
and none extra, and natalChart confirmed every switch at its millisecond
(docs/evidence/birth-window/RESULTS.md); on rc.16's build, after the time
basis and the nutation, again (docs/evidence/birth-window/rc16/).
In one thread, a 10-minute window takes about 2 ms, two hours about 20 ms and
a whole day about 0.23 s (medians, Node.js 22; timing.json). A window over a
node ingress takes from seconds to a few minutes, as the node is compared at
every millisecond of its flicker (1,950 s around the ingress of 2032-11-25:
77 s, 18,315 switches); an unresolved stretch costs a fraction of a second.
Within 1e-8° of the polar limit each passage of a sensitive RAMC adds about
20 s, and at 89.999999° a 48-hour window takes up to three minutes
(Alcabitius).
Other flags are polar-fallback, when a cell uses whole-sign houses in place
of Placidus or Koch, bound-exceeded and node-unresolved. Invalid input
throws RangeError, including a window outside REFERENCE_SPAN and a
latitude within 1e-6° of either pole, where the ascendant is undefined or
turns too fast for the search. A search that would evaluate more than two
million instants outside the unresolved intervals throws WindowBudgetError,
which is an Error and not a RangeError. The entry point carries the
ephemeris and is separate from the root entry point, so that the root does not
grow.
DateInput values passed to the calculation APIs accept a valid Date, a finite
epoch-millisecond timestamp representable by JavaScript Date, or these ISO
string forms:
YYYY-MM-DD, interpreted as midnight UTC using the proleptic Gregorian
calendar. This convenience does not infer a birthplace's local midnight.YYYY-MM-DDTHH:mm[:ss[.sss]]Z, or the same date-time with an explicit
+HH:mm or -HH:mm offset. Fractional seconds, when supplied, have one to
three digits. ISO expanded years use a sign and six digits, such as
-000001-01-01T00:00:00Z; years 0000–0099 are not shifted to 1900–1999.Invalid calendar dates, rollovers such as February 30 or 24:00, leap seconds,
unresolved local date-times, locale-specific strings, excessive fractional
precision, non-finite values, and other input types throw RangeError before
calculation. Existing Date values cannot reveal whether a caller previously
normalized an invalid date; pass the original string when validation is needed.
Date and numeric inputs retain JavaScript's millisecond resolution.
Z, +00:00, and -00:00 identify the same UTC instant. Results normalize to
Date and do not retain the original offset or local-zone provenance; in
particular, RFC 3339's -00:00 convention
indicating an unknown local offset is not
preserved. Resolve daylight-saving gaps/folds and historical local-time rules
before calling these APIs. Accepted date syntax is not an accuracy guarantee
outside the documented reference coverage.
Every valid Date is accepted as input, but the ephemeris is evaluated only
inside EPHEMERIS_SPAN: Terrestrial Time from 0001-04-30T12:00 to
3998-09-03T12:00, J2000 ± 730,000 days, the years astronomy-engine tabulates.
Its source says of instants beyond them: "The target time is outside the year
range 0000..4000. Calculate it by crawling backward from 0000 or forward from
4000. FIXFIXFIX - This is super slow." rc.13 still evaluated them: a position
at year 30,000 took more than 20 s and put the Sun 29° off the ecliptic. Now
positions, and the charts, transits, synastry, progressions, declinations,
points, crossings and returns built on them, throw RangeError at once for
such an instant, with a message that names the span. The check is on
Terrestrial Time, and it covers the speed samples ±0.001 day around the
instant (±0.25 day for the true nodes), so an instant must lie six hours
inside. On the model ΔT clock every instant from 0001-05-01T00:00Z to
3998-09-02T00:00Z qualifies; a pinned deltaT moves TT and is checked as
given. astronomy-engine reports any other failure by throwing a string, which
reaches the caller as a RangeError whose cause is the original value.
Inside the span, results are computed but not verified: accuracy was compared
only within REFERENCE_SPAN, and it falls away from it. The Sun's ecliptic
latitude, which in reality stays within about 1.2″ of zero, comes out as
−9.9″ on 1000-06-01, +13.3″ on 3000-06-01 and −57.5″ on 3998-09-02.
Birth settings accept only a houseSystem from the thirteen above, a boolean
timeKnown and a timeScale (see Time). Omitting them defaults to "whole",
true and "utc"; explicit null and other unsupported values throw
RangeError, including when coordinates are absent. Latitude and longitude
must be supplied together as finite numbers within [-90, 90] and
[-180, 180] respectively.
A chart reads the Earth's rotation (sidereal time, the angles) on UT1 and the
planets on Terrestrial Time (TT). utc is on UTC unless timeScale says
"ut1" or "tt"; any other value throws RangeError.
finals2000A.all of 2026-09-24,
observed then predicted. ΔT = TT − UT1 is then reported with
model: "iers-utc/1".zodiacs-deltat/1: Stephenson, Morrison &
Hohenkerk 2016 (CC BY 4.0) up to 1941, then USNO and IERS observations,
Bulletin A's predictions and a damped extrapolation, with a 1-σ band. After
the table UT1 − UTC is taken as 0 within ±0.9 s.deltaT (seconds) in a birth input pins ΔT; the chart reports
model: "pinned".chart.deltaT is { seconds, sigma, model, table, tableDigest, segment }, and
chart.timeScale says how the instant became UT1 and TT ({ input, basis, ut1MinusUtc, leapSeconds }). @zodiacs/engine/deltat exports the model with
no dependencies. astronomy-engine keeps one ΔT for its whole module; every
engine call leaves the model installed, and code that calls astronomy-engine
directly should install it too: SetDeltaTFunction(deltaT).
@zodiacs/engine/geo reads wall times before 1970 on the tzdb 2025c history
with backzone, shipped as 16 lazily imported chunks: prepareLocalTime(date, timeZone) loads the one a zone needs, and a wall time before 1970 throws until
it has. From 1970 the host's Intl answers. A longitude reads a time in a
zone's local mean time era on the birthplace's own mean time; calendar: "julian" takes an Old Style date, and country adds a note from the country's
Gregorian adoption. Each resolution names the tzdb version, the transition
behind the offset and its cause (dst, legal-change or date-line).
docs/time.md describes every field.
Longitudes, the sidereal time and the obliquity of date use the IAU 2000B
nutation (McCarthy & Luzum 2003; IERS Conventions 2003, chapter 5), all 77 of
its luni-solar terms and its two fixed planetary offsets, which the engine
evaluates itself (src/nutation.ts, transcribed from NOVAS C 3.1). Its Δψ and
Δε equal ERFA's nut00b within 1e-10″ at 101 instants from 1800 to 2200
(src/nutation.test.ts). astronomy-engine's vectors on the J2000 mean equator
are turned to the ecliptic of date with astronomy-engine's own IAU 2006
precession (src/frame.ts) and that nutation. Measured against ERFA's IAU
2006/2000A chain applied to the same vectors at 4,001 instants from 1800 to
2200, that rotation puts a longitude at most 0.003691″ off, and the ascendant
and midheaven at latitudes within 60° at most 0.006284″ and 0.003358″ off; with
astronomy-engine's five-term nutation, used until this change, they were up to
0.2520″, 0.8235″ and 0.2457″ off
(docs/evidence/nutation-2026-09-29/results/erfa-frame.json). The equation of
the equinoxes adds the two largest IAU 2000 complementary terms (IERS
Conventions 2010, table 5.2e). The opt-in entries use the same precession,
nutation and sidereal time: @zodiacs/engine/calc for its true-of-date
frames and its topocentric observer, @zodiacs/engine/window for the angles,
and @zodiacs/engine/sky for its apparent places and the observer's
sidereal angle.
The ephemeris is powered by the MIT-licensed astronomy-engine. Tests compare
modern and historical positions with public JPL Horizons vectors and exercise
astronomical and geometric invariants. See LICENSING.md for the
full provenance audit and the explicit Swiss Ephemeris exclusion.
The package's licence expression is MIT AND CC-BY-4.0: the code is MIT
(LICENSE), and the 32 values of Stephenson, Morrison & Hohenkerk's
Table S15 in the package's ΔT module (@zodiacs/engine/deltat, which the build
places in a shared chunk under dist/ that dist/deltat.js re-exports) are
CC BY 4.0, attributed in NOTICE.
The npm package contains no place database. It carries tzdb 2025c's zone histories before 1970, the IERS leap-second list, an IERS UT1 − UTC table, a table of Gregorian adoption dates from public-domain sources, 22 catalogue values for the stars of the Vedic ayanamsas, and the IAU 2000B nutation series from NOVAS C 3.1, a US Government work; NOTICE records their sources and the GeoNames attribution, and downstream users should retain it. LICENSING.md gives the terms of each.
@zodiacs/engine/internal and @zodiacs/engine/internal/math are private
compatibility boundaries for Zodiacs.org. They let the site consume the exact
package implementation while keeping its scanner-oriented functions and lazy
bundle boundary intact. They are not covered by semantic-versioning guarantees;
third-party code must use the documented entry points: the root, /calc,
/geo, /houses, /sky, /techniques, /timing, /vedic, /window,
/receipt, /conventions, /crossings and /deltat.
The optional geo client shares in-flight requests and keeps validated index/shards. A rejected fetch, unsuccessful HTTP response or JSON parsing failure is returned to current callers with its original rejection reason. A later explicit preload/search call may retry the failed resource. There is no automatic retry loop, backoff or per-caller cancellation API. A custom fetch may bind its own abort signal; the client does not reset that signal.
Parseable JSON must match the compact v1 asset format: index string tables and
0/a–z shard keys, with eight-field city rows containing valid table indices,
integer coordinates in hundredths of a degree (including ±90°/±180°), and
nonnegative integer populations. An invalid index or shard rejects with
TypeError("Invalid GeoNames index data.") or
TypeError("Invalid GeoNames shard data.") and is evicted for a later explicit
retry. The entire requested shard is checked before returning any results;
other successful caches remain available. Empty region/country labels and
Unicode names are retained. Timezone identifiers are checked as nonempty strings,
without requiring support in the current host's timezone database. Validation
does not authenticate place facts, check that the advertised count equals all
shards, or refresh a previously valid index when a shard changes independently.
In-range indices in a shard from another dataset generation can silently select
the wrong cached country or timezone; this v1 format has no content or generation
identity to detect that mismatch. Serve matching index/shards together.
preload() returns metadata snapshots: its arrays do not expose mutable cache
state.
These checks apply to parsed JSON data. A custom fetch implementation is trusted code, not a sandbox: accessors or iterators it supplies in non-JSON objects may execute during validation.
The optional @zodiacs/engine/receipt entry point is a Zodiacs draft for one
natal-chart envelope. It preserves full numerical precision and separates a
requested house system from the actual computed system. It does not calculate
an ephemeris, fetch an imported URL, execute extensions, save a profile or change
account sync v1. A valid envelope is a structurally checked claim, not an
attestation that its positions or provenance are authentic.
import { natalChart } from "@zodiacs/engine";
import {
createNatalEnvelope,
serializeNatalEnvelope,
parseNatalEnvelope,
natalReplayInput,
redactNatalEnvelope
} from "@zodiacs/engine/receipt";
const chart = natalChart({
utc: "2001-12-21T09:00:00Z",
latitude: 78.2232,
longitude: 15.6267,
houseSystem: "placidus"
});
const encoded = serializeNatalEnvelope(createNatalEnvelope(chart));
const decoded = parseNatalEnvelope(encoded);
if (decoded.ok) {
// Replay still requests Placidus even though this result used whole-sign.
const replay = natalChart(natalReplayInput(decoded.envelope));
console.log(replay.houses?.system);
console.log(redactNatalEnvelope(decoded.envelope));
}
Capture original time-resolution and package/runtime facts while they are
available. Unknown birth time does not establish a noon convention: an explicit
08:30 reference remains 08:30, while unavailable historical context remains
unavailable. Imported package hashes and version strings are untrusted claims.
Pass the original validated ISO string as sourceInstant when available;
normalization alone cannot recover its original offset spelling. Captured local
resolution is checked arithmetically, without consulting the current timezone
database or authenticating the historical claim.
Receipts from 0.1.1-rc.8 on name the ephemeris that computed them, as
receipt.engine.ephemeris ({ name: "astronomy-engine", version: "2.1.19" },
exported as EPHEMERIS); the dependency is pinned to that exact version, and
a current receipt without it is refused. Their conventions say what the
positions are corrected for (aberrated-geocentric-ecliptic-of-date;no-deflection)
and that the Moon has neither correction. Receipts from rc.3 to rc.7 are still
read, each under the conventions its engine recorded, and a set is accepted
only from the engine versions that wrote it.
The time-basis sets, written from 0.1.1-rc.15 on, also record the instant's
scale (receipt.timeScale) and its time basis (result.timeScale), and a
local resolution records the calendar, the tzdb version and form, the clock,
the transition behind the offset and any birthplace mean time, each checked
against the offsets and flags. The set written from 0.1.1-rc.16 to 1.0.0
also names the nutation, the engine's IAU 2000B (nutation), and takes the
Moon from astronomy-engine's geocentric series turned by the engine's frame
(moonPosition); rc.15's set, which does not name the nutation, stays
readable. Receipts under the rc.8 set (rc.8 to rc.14), which read the instant
as UT1, stay readable and replay as UTC requests (see docs/time.md). A
receipt's engine, ephemeris and package versions must be SemVer 2.0.0
versions. The codec accepts an earlier set only under the versions that wrote
it, rc.15's under 0.1.1-rc.15 alone, and compares the engine's version in
SemVer order with the release that brought each set and each house system: a
receipt names the engine that computed its chart, so rc.16's set and the
current one are accepted from 0.1.1-rc.16 on, and one that names a version
before it, such as 0.1.1-rc.15.1 or 0.1.1-beta, is refused.
The current set, written from 1.1.0-rc.1 on, is rc.16's with
conventionIds, and its receipts list, as receipt.conventionIds, every id in
the syntax of the Zodiacs conventions registry
(zodiacs.org/developers/conventions)
that the receipt and its result name, in code-unit order, each once: each
conventions and coverage field's key and value (zodiac:tropical), the
requested and actual house systems (house:placidus), why there are no
houses, each flag, the reference, the instant's scale (time-scale:utc), the
ephemeris (ephemeris:[email protected]), and the result's ΔT model and
segment, time basis and source of UT1 − UTC (time-basis:iers). The codec
refuses ids that differ from those the envelope's fields name. Its envelopes
require the feature convention-ids, so an earlier engine refuses them as
unsupported_feature (docs/versioning.md). @zodiacs/engine/conventions
exports the rule as natalConventionIds(envelope), which also names a 1.0.0
envelope's ids, with conventionId, parseConventionId (the value is
everything after the first colon) and conventionAddress, an id's address on
the registry's page; each throws a RangeError for a malformed id.
natalReplayInput recovers the recorded request. It does not select or install
the original engine. Recalculation with another engine, ephemeris dependency or
runtime may differ; verify trusted artifact identity and runtime provenance
before claiming reproducible results. A matching imported version label alone
is insufficient. Displaying the stored result does not require recalculation.
The redacted diagnostic uses fixed fields; it does not copy birth details, numerical positions, arbitrary metadata, extensions, raw parser errors or stable hashes. Redacted does not mean anonymous.
Imports are limited to one chart and 64 KiB of UTF-8 JSON, with bounded depth, node count and data arrays. Unknown required features or schema versions are rejected explicitly. Optional data belongs in bounded extensions and is never executed or rendered by this codec. Keep encrypted account payloads and legacy positions-only links in their existing formats until an explicit adapter and safe downgrade policy are reviewed. Do not infer a legacy user's requested house system from a stored fallback result.
@zodiacs/engine/vedic provides the sidereal zodiac: ayanamsas, sidereal
charts, nakshatras, vargas, KP sub-lords and dashas (see docs/vedic.md).@zodiacs/engine/sky provides rise, set and transit times for an observer
and planetary hours (see docs/sky.md).@zodiacs/engine/window partitions a birth-time window into cells within
which the chart's signs, houses and aspects are constant (see Birth-time
windows).@zodiacs/engine/conventions, new in 1.1.0-rc.1, gives the ids of the
conventions registry a natal receipt names, and each id's address (see
Draft natal receipts). It imports no module of another entry point.