caelus 0.18.0 → 0.20.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.
@@ -242,6 +242,44 @@ export declare class Engine {
242
242
  * @returns Sorted catalog star names.
243
243
  */
244
244
  starNames(): string[];
245
+ /**
246
+ * Fixed-star conjunctions in a chart: each body within `orb` of a catalog
247
+ * star, in the chart's own zodiac. Feed the result to
248
+ * {@link interpretationContext} as `stars` to project `star` fact atoms (the
249
+ * Chart itself carries no star catalog).
250
+ *
251
+ * @param chart A chart from {@link Engine.chart} / {@link Engine.chartAt}.
252
+ * @param opts `orb` (default 1°); `stars` to restrict to named stars (then no
253
+ * magnitude filter); else `maxMag` keeps only stars brighter than it
254
+ * (default 2.5) so obscure catalog entries do not flood the result.
255
+ * @returns Conjunctions sorted by increasing orb.
256
+ */
257
+ starConjunctions(chart: Chart, opts?: {
258
+ orb?: number;
259
+ maxMag?: number;
260
+ stars?: string[];
261
+ }): {
262
+ body: string;
263
+ star: string;
264
+ orb: number;
265
+ }[];
266
+ /**
267
+ * The seven Hermetic lots of a chart, each placed by sign and house. Sect is
268
+ * read from the Sun (above the horizon -> a day chart). Feed the result to
269
+ * {@link interpretationContext} as `lots` to project `lot` fact atoms.
270
+ *
271
+ * @param chart A chart from {@link Engine.chart} / {@link Engine.chartAt}; it
272
+ * must carry the seven classical planets.
273
+ * @returns One entry per lot with its longitude, sign, `signDeg`, and house,
274
+ * or an empty array if a required planet is absent.
275
+ */
276
+ lots(chart: Chart): {
277
+ lot: string;
278
+ lon: number;
279
+ sign: string;
280
+ signDeg: number;
281
+ house: number;
282
+ }[];
245
283
  private lonOnly;
246
284
  /**
247
285
  * Apparent geocentric ecliptic longitude of a body, in degrees `[0, 360)`,
package/dist/src/chart.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /** astroengine chart -- public API: natal charts, aspects, retrogrades. */
2
2
  import { DEG, mod, jdTT, julianDay, ChebSeries, planetApparent, sunApparent, moonApparentSeries, moonApparentPrecise, plutoApparent, chironApparent, meanNode, trueNodeSeries, trueNodePrecise, equatorial, ayanamsa, AYANAMSA_J2000, meanLilith, topocentricEcl, oscApogeePrecise, oscApogeeSeries, KeplerOrbit, trueObliquity, nutation, plutoHeliocentric, vsopHeliocentric, precessEcliptic, J2000, } from "./core.js";
3
3
  import { starApparent } from "./stars.js";
4
+ import { hermeticLots, HERMETIC_LOTS } from "./lots.js";
4
5
  import * as H from "./houses.js";
5
6
  const TWO_PI = 2 * Math.PI;
6
7
  export const BODIES = [
@@ -295,6 +296,68 @@ export class Engine {
295
296
  starNames() {
296
297
  return Object.keys(this.data.fixedStars?.stars ?? {}).sort();
297
298
  }
299
+ /**
300
+ * Fixed-star conjunctions in a chart: each body within `orb` of a catalog
301
+ * star, in the chart's own zodiac. Feed the result to
302
+ * {@link interpretationContext} as `stars` to project `star` fact atoms (the
303
+ * Chart itself carries no star catalog).
304
+ *
305
+ * @param chart A chart from {@link Engine.chart} / {@link Engine.chartAt}.
306
+ * @param opts `orb` (default 1°); `stars` to restrict to named stars (then no
307
+ * magnitude filter); else `maxMag` keeps only stars brighter than it
308
+ * (default 2.5) so obscure catalog entries do not flood the result.
309
+ * @returns Conjunctions sorted by increasing orb.
310
+ */
311
+ starConjunctions(chart, opts = {}) {
312
+ const catalog = this.data.fixedStars?.stars;
313
+ if (!catalog)
314
+ return [];
315
+ const orbLimit = opts.orb ?? 1.0;
316
+ const names = opts.stars ?? Object.keys(catalog);
317
+ const useMag = opts.stars === undefined;
318
+ const maxMag = opts.maxMag ?? 2.5;
319
+ const out = [];
320
+ for (const name of names) {
321
+ const s = catalog[name];
322
+ if (!s || (useMag && s.mag > maxMag))
323
+ continue;
324
+ const starLon = this.fixedStar(name, chart.jdUt, { zodiac: chart.zodiac }).lon;
325
+ for (const [body, p] of Object.entries(chart.bodies)) {
326
+ if (!p)
327
+ continue;
328
+ const sep = Math.abs(mod(p.lon - starLon + 180, 360) - 180);
329
+ if (sep <= orbLimit)
330
+ out.push({ body, star: name, orb: sep });
331
+ }
332
+ }
333
+ out.sort((a, b) => a.orb - b.orb);
334
+ return out;
335
+ }
336
+ /**
337
+ * The seven Hermetic lots of a chart, each placed by sign and house. Sect is
338
+ * read from the Sun (above the horizon -> a day chart). Feed the result to
339
+ * {@link interpretationContext} as `lots` to project `lot` fact atoms.
340
+ *
341
+ * @param chart A chart from {@link Engine.chart} / {@link Engine.chartAt}; it
342
+ * must carry the seven classical planets.
343
+ * @returns One entry per lot with its longitude, sign, `signDeg`, and house,
344
+ * or an empty array if a required planet is absent.
345
+ */
346
+ lots(chart) {
347
+ const b = chart.bodies;
348
+ const need = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn"];
349
+ if (need.some((k) => !b[k]))
350
+ return [];
351
+ const day = (b.sun.house >= 7); // Sun above the horizon (houses 7-12)
352
+ const h = hermeticLots(chart.angles.asc, day, b.sun.lon, b.moon.lon, b.mercury.lon, b.venus.lon, b.mars.lon, b.jupiter.lon, b.saturn.lon);
353
+ return HERMETIC_LOTS.map((lot) => {
354
+ const lon = mod(h[lot], 360);
355
+ return {
356
+ lot, lon, sign: SIGNS[Math.floor(lon / 30)], signDeg: mod(lon, 30),
357
+ house: houseIndex(lon, chart.cusps),
358
+ };
359
+ });
360
+ }
298
361
  lonOnly(body, jdUt, mode, topo) {
299
362
  const jde = jdTT(jdUt);
300
363
  let [lon, lat, dist] = this.ecliptic(body, jde);
@@ -29,7 +29,9 @@ export * from "./rajayoga.js";
29
29
  export * from "./patterns.js";
30
30
  export * from "./signature.js";
31
31
  export * from "./interpretation.js";
32
+ export * from "./interpretation-enrich.js";
32
33
  export * from "./interpret.js";
34
+ export * from "./relational.js";
33
35
  export * from "./brief.js";
34
36
  export * from "./provenance.js";
35
37
  export * from "./anchored.js";
package/dist/src/index.js CHANGED
@@ -29,7 +29,9 @@ export * from "./rajayoga.js";
29
29
  export * from "./patterns.js";
30
30
  export * from "./signature.js";
31
31
  export * from "./interpretation.js";
32
+ export * from "./interpretation-enrich.js";
32
33
  export * from "./interpret.js";
34
+ export * from "./relational.js";
33
35
  export * from "./brief.js";
34
36
  export * from "./provenance.js";
35
37
  export * from "./anchored.js";
@@ -63,6 +63,69 @@ export declare function hasDispositor(filter?: {
63
63
  export declare function hasReception(filter?: {
64
64
  body?: string;
65
65
  }): Selector;
66
+ /** Matches a fixed-star conjunction by the catalog star and/or the body on it. */
67
+ export declare function hasStar(filter?: {
68
+ body?: string;
69
+ star?: string;
70
+ }): Selector;
71
+ /** Matches a Hermetic lot by name, and/or its sign or house. */
72
+ export declare function hasLot(filter?: {
73
+ lot?: string;
74
+ sign?: string;
75
+ house?: number;
76
+ }): Selector;
77
+ /** Matches transit-to-natal aspect atoms. */
78
+ export declare function hasTransit(filter?: {
79
+ transit?: string;
80
+ natal?: string;
81
+ aspect?: string;
82
+ phase?: string;
83
+ minStrength?: number;
84
+ }): Selector;
85
+ /** Matches synastry aspect or house-overlay atoms. */
86
+ export declare function hasSynastry(filter?: {
87
+ mode?: "aspect" | "overlay";
88
+ a?: string;
89
+ b?: string;
90
+ aspect?: string;
91
+ body?: string;
92
+ partner?: "a" | "b";
93
+ house?: number;
94
+ }): Selector;
95
+ /** Matches a composite midpoint placement. */
96
+ export declare function hasComposite(filter?: {
97
+ body?: string;
98
+ sign?: string;
99
+ }): Selector;
100
+ /** Matches an active time-lord period. */
101
+ export declare function hasTimelord(filter?: {
102
+ system?: "profection" | "zr" | "firdaria" | "dasha";
103
+ level?: string;
104
+ lord?: string;
105
+ }): Selector;
106
+ /** Matches finer essential-dignity facts (term, face, triplicity, almuten). */
107
+ export declare function hasDignityFine(filter?: {
108
+ facet?: "term" | "face" | "triplicity" | "almuten";
109
+ body?: string;
110
+ ruler?: string;
111
+ }): Selector;
112
+ /** Matches a nakshatra placement. */
113
+ export declare function hasNakshatra(filter?: {
114
+ body?: string;
115
+ name?: string;
116
+ lord?: string;
117
+ }): Selector;
118
+ /** Matches a varga (divisional chart) placement. */
119
+ export declare function hasVarga(filter?: {
120
+ division?: number;
121
+ body?: string;
122
+ sign?: string;
123
+ }): Selector;
124
+ /** Matches a classical yoga. */
125
+ export declare function hasYoga(filter?: {
126
+ yoga?: string;
127
+ body?: string;
128
+ }): Selector;
66
129
  /** Matches only when every selector matches; returns the union of their atoms. */
67
130
  export declare function matchAll(...sels: Selector[]): Selector;
68
131
  /** Matches when any selector matches; returns the atoms from those that did. */
@@ -61,6 +61,99 @@ export function hasReception(filter = {}) {
61
61
  return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "reception"
62
62
  && (filter.body === undefined || a.bodies.includes(filter.body))));
63
63
  }
64
+ /** Matches a fixed-star conjunction by the catalog star and/or the body on it. */
65
+ export function hasStar(filter = {}) {
66
+ return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "star"
67
+ && (filter.body === undefined || a.body === filter.body)
68
+ && (filter.star === undefined || a.star === filter.star)));
69
+ }
70
+ /** Matches a Hermetic lot by name, and/or its sign or house. */
71
+ export function hasLot(filter = {}) {
72
+ return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "lot"
73
+ && (filter.lot === undefined || a.lot === filter.lot)
74
+ && (filter.sign === undefined || a.sign === filter.sign)
75
+ && (filter.house === undefined || a.house === filter.house)));
76
+ }
77
+ /** Matches transit-to-natal aspect atoms. */
78
+ export function hasTransit(filter = {}) {
79
+ return (ctx) => hit(ctx.atoms.filter((a) => {
80
+ if (a.kind !== "transit")
81
+ return false;
82
+ if (filter.transit !== undefined && a.transit !== filter.transit)
83
+ return false;
84
+ if (filter.natal !== undefined && a.natal !== filter.natal)
85
+ return false;
86
+ if (filter.aspect !== undefined && a.aspect !== filter.aspect)
87
+ return false;
88
+ if (filter.phase !== undefined && a.phase !== filter.phase)
89
+ return false;
90
+ if (filter.minStrength !== undefined && a.strength < filter.minStrength)
91
+ return false;
92
+ return true;
93
+ }));
94
+ }
95
+ /** Matches synastry aspect or house-overlay atoms. */
96
+ export function hasSynastry(filter = {}) {
97
+ return (ctx) => hit(ctx.atoms.filter((a) => {
98
+ if (a.kind !== "synastry")
99
+ return false;
100
+ if (filter.mode !== undefined && a.mode !== filter.mode)
101
+ return false;
102
+ if (filter.a !== undefined && a.a !== filter.a)
103
+ return false;
104
+ if (filter.b !== undefined && a.b !== filter.b)
105
+ return false;
106
+ if (filter.aspect !== undefined && a.aspect !== filter.aspect)
107
+ return false;
108
+ if (filter.body !== undefined && a.body !== filter.body)
109
+ return false;
110
+ if (filter.partner !== undefined && a.partner !== filter.partner)
111
+ return false;
112
+ if (filter.house !== undefined && a.house !== filter.house)
113
+ return false;
114
+ return true;
115
+ }));
116
+ }
117
+ /** Matches a composite midpoint placement. */
118
+ export function hasComposite(filter = {}) {
119
+ return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "composite"
120
+ && (filter.body === undefined || a.body === filter.body)
121
+ && (filter.sign === undefined || a.sign === filter.sign)));
122
+ }
123
+ /** Matches an active time-lord period. */
124
+ export function hasTimelord(filter = {}) {
125
+ return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "timelord"
126
+ && (filter.system === undefined || a.system === filter.system)
127
+ && (filter.level === undefined || a.level === filter.level)
128
+ && (filter.lord === undefined || a.lord === filter.lord)));
129
+ }
130
+ /** Matches finer essential-dignity facts (term, face, triplicity, almuten). */
131
+ export function hasDignityFine(filter = {}) {
132
+ return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "dignity"
133
+ && (filter.facet === undefined || a.facet === filter.facet)
134
+ && (filter.body === undefined || a.body === filter.body)
135
+ && (filter.ruler === undefined || a.ruler === filter.ruler)));
136
+ }
137
+ /** Matches a nakshatra placement. */
138
+ export function hasNakshatra(filter = {}) {
139
+ return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "nakshatra"
140
+ && (filter.body === undefined || a.body === filter.body)
141
+ && (filter.name === undefined || a.name === filter.name)
142
+ && (filter.lord === undefined || a.lord === filter.lord)));
143
+ }
144
+ /** Matches a varga (divisional chart) placement. */
145
+ export function hasVarga(filter = {}) {
146
+ return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "varga"
147
+ && (filter.division === undefined || a.division === filter.division)
148
+ && (filter.body === undefined || a.body === filter.body)
149
+ && (filter.sign === undefined || a.sign === filter.sign)));
150
+ }
151
+ /** Matches a classical yoga. */
152
+ export function hasYoga(filter = {}) {
153
+ return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "yoga"
154
+ && (filter.yoga === undefined || a.yoga === filter.yoga)
155
+ && (filter.body === undefined || a.bodies.includes(filter.body))));
156
+ }
64
157
  // ----------------------------------------------------------------- combinators
65
158
  /** Matches only when every selector matches; returns the union of their atoms. */
66
159
  export function matchAll(...sels) {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Enrich an {@link interpretationContext} with diachronic and Vedic facts at a
3
+ * target instant — transits, time-lords, nakshatras, vargas, yogas.
4
+ */
5
+ import { type Chart, type Engine, type Zodiac } from "./chart.js";
6
+ import type { ContextOptions } from "./interpretation.js";
7
+ export interface EnrichTarget {
8
+ jd: number;
9
+ lat: number;
10
+ lonEast: number;
11
+ zodiac?: Zodiac;
12
+ }
13
+ export interface EnrichFlags {
14
+ /** Project transit-to-natal aspects. Default true. */
15
+ transits?: boolean;
16
+ /** Project profection, ZR, firdaria, dasha. Default true. */
17
+ timelords?: boolean;
18
+ /** Project nakshatras, D9, yogas. Default true when chart zodiac is sidereal. */
19
+ vedic?: boolean;
20
+ /** Max orb for transit aspects. Default 3. */
21
+ transitOrb?: number;
22
+ }
23
+ /**
24
+ * Build {@link ContextOptions} extras for a natal chart evaluated at `target`.
25
+ * Merge the result into `interpretationContext(chart, { ...base, ...extras })`.
26
+ */
27
+ export declare function enrichContextOptions(engine: Engine, chart: Chart, target: EnrichTarget, flags?: EnrichFlags): Pick<ContextOptions, "transits" | "timelords" | "vedic">;
28
+ /** Synastry and composite atoms for two natal charts (A is the projection base). */
29
+ export declare function enrichSynastryOptions(engine: Engine, chartA: Chart, chartB: Chart, opts?: {
30
+ orb?: number;
31
+ zodiac?: Zodiac;
32
+ }): Pick<ContextOptions, "synastry" | "composite">;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Enrich an {@link interpretationContext} with diachronic and Vedic facts at a
3
+ * target instant — transits, time-lords, nakshatras, vargas, yogas.
4
+ */
5
+ import { BODIES } from "./chart.js";
6
+ import { firdariaAt } from "./firdaria.js";
7
+ import { profectionAt } from "./profections.js";
8
+ import { zrAt } from "./releasing.js";
9
+ import { compositePlacements, synastryAspects, synastryOverlays, transitAspects, } from "./relational.js";
10
+ import { vimshottariAt } from "./vedic.js";
11
+ import { yogasAt } from "./yogas.js";
12
+ /**
13
+ * Build {@link ContextOptions} extras for a natal chart evaluated at `target`.
14
+ * Merge the result into `interpretationContext(chart, { ...base, ...extras })`.
15
+ */
16
+ export function enrichContextOptions(engine, chart, target, flags = {}) {
17
+ const zodiac = target.zodiac ?? chart.zodiac;
18
+ const { lat, lonEast } = target;
19
+ const out = {};
20
+ if (flags.transits !== false) {
21
+ out.transits = transitAspects(chart, engine, target.jd, {
22
+ maxOrb: flags.transitOrb ?? 3, zodiac,
23
+ });
24
+ }
25
+ if (flags.timelords !== false) {
26
+ const prof = profectionAt(engine, chart.jdUt, target.jd, lat, lonEast, zodiac);
27
+ const zr = zrAt(engine, chart.jdUt, target.jd, lat, lonEast);
28
+ const fir = firdariaAt(engine, chart.jdUt, target.jd, lat, lonEast);
29
+ const dasha = vimshottariAt(engine, chart.jdUt, target.jd, "sidereal:lahiri");
30
+ out.timelords = {
31
+ profection: prof,
32
+ zr: {
33
+ l1: zr.l1, l2: zr.l2, l3: zr.l3, l4: zr.l4, lot: zr.lot,
34
+ },
35
+ firdaria: { major: fir.major, sub: fir.sub, day: fir.day },
36
+ dasha: {
37
+ maha: dasha.maha, antar: dasha.antar ?? null,
38
+ pratyantar: dasha.pratyantar ?? null, moon_nakshatra: dasha.moon_nakshatra,
39
+ },
40
+ };
41
+ }
42
+ const wantVedic = flags.vedic ?? zodiac.startsWith("sidereal");
43
+ if (wantVedic) {
44
+ out.vedic = {
45
+ nakshatraBodies: ["moon", "sun", "mars", "mercury", "jupiter", "venus", "saturn"],
46
+ vargas: [9],
47
+ yogas: yogasAt(engine, chart.jdUt, lat, lonEast, "sidereal:lahiri"),
48
+ };
49
+ }
50
+ return out;
51
+ }
52
+ /** Synastry and composite atoms for two natal charts (A is the projection base). */
53
+ export function enrichSynastryOptions(engine, chartA, chartB, opts = {}) {
54
+ const orb = opts.orb ?? 4;
55
+ const zodiac = opts.zodiac ?? chartA.zodiac;
56
+ return {
57
+ synastry: {
58
+ aspects: synastryAspects(chartA, chartB, orb),
59
+ overlays: synastryOverlays(chartA, chartB),
60
+ },
61
+ composite: compositePlacements(engine, chartA.jdUt, chartB.jdUt, BODIES, zodiac),
62
+ };
63
+ }
@@ -3,8 +3,10 @@ import type { AspectPhase } from "./electional.js";
3
3
  import { ChartPattern } from "./patterns.js";
4
4
  import { ChartSignature } from "./signature.js";
5
5
  import type { Realm, Certainty } from "./provenance.js";
6
+ import type { Profection } from "./profections.js";
7
+ import type { CompositePlacement, SynastryAspectHit, SynastryOverlays, TransitHit } from "./relational.js";
6
8
  /** Atom kinds in an {@link InterpretationContext}. */
7
- export type FactKind = "placement" | "aspect" | "pattern" | "signature" | "angle" | "dispositor" | "reception";
9
+ export type FactKind = "placement" | "aspect" | "pattern" | "signature" | "angle" | "dispositor" | "reception" | "star" | "lot" | "transit" | "synastry" | "composite" | "timelord" | "dignity" | "nakshatra" | "varga" | "yoga";
8
10
  interface FactAtomBase {
9
11
  /** Stable, content-addressable id, e.g. `"placement:mars"` or
10
12
  * `"aspect:mars~saturn:square"`. Interpretations cite this. */
@@ -72,7 +74,83 @@ export interface ReceptionAtom extends FactAtomBase {
72
74
  * else a sorted pair for a mixed reception (e.g. `"domicile-exaltation"`). */
73
75
  by: string;
74
76
  }
75
- export type FactAtom = PlacementAtom | AspectAtom | PatternAtom | SignatureAtom | AngleAtom | DispositorAtom | ReceptionAtom;
77
+ export interface StarAtom extends FactAtomBase {
78
+ kind: "star";
79
+ /** The body conjunct the fixed star. */
80
+ body: string;
81
+ /** Catalog star name (see {@link Engine.starNames}). */
82
+ star: string;
83
+ /** Orb from exact conjunction, degrees. */
84
+ orb: number;
85
+ }
86
+ export interface LotAtom extends FactAtomBase {
87
+ kind: "lot";
88
+ /** Hermetic lot name, e.g. `"fortune"` (see {@link HERMETIC_LOTS}). */
89
+ lot: string;
90
+ sign: string;
91
+ signDeg: number;
92
+ house: number;
93
+ }
94
+ export interface TransitAtom extends FactAtomBase {
95
+ kind: "transit";
96
+ transit: string;
97
+ natal: string;
98
+ aspect: string;
99
+ orb: number;
100
+ phase: AspectPhase;
101
+ strength: number;
102
+ natalHouse: number;
103
+ }
104
+ export interface SynastryAtom extends FactAtomBase {
105
+ kind: "synastry";
106
+ mode: "aspect" | "overlay";
107
+ a?: string;
108
+ b?: string;
109
+ aspect?: string;
110
+ orb?: number;
111
+ strength?: number;
112
+ body?: string;
113
+ partner?: "a" | "b";
114
+ house?: number;
115
+ }
116
+ export interface CompositeAtom extends FactAtomBase {
117
+ kind: "composite";
118
+ body: string;
119
+ sign: string;
120
+ signDeg: number;
121
+ }
122
+ export interface TimelordAtom extends FactAtomBase {
123
+ kind: "timelord";
124
+ system: "profection" | "zr" | "firdaria" | "dasha";
125
+ level: string;
126
+ lord: string;
127
+ sign?: string;
128
+ }
129
+ export interface DignityAtom extends FactAtomBase {
130
+ kind: "dignity";
131
+ facet: "term" | "face" | "triplicity" | "almuten";
132
+ body: string;
133
+ ruler?: string;
134
+ }
135
+ export interface NakshatraAtom extends FactAtomBase {
136
+ kind: "nakshatra";
137
+ body: string;
138
+ name: string;
139
+ pada: number;
140
+ lord: string;
141
+ }
142
+ export interface VargaAtom extends FactAtomBase {
143
+ kind: "varga";
144
+ division: number;
145
+ body: string;
146
+ sign: string;
147
+ }
148
+ export interface YogaAtom extends FactAtomBase {
149
+ kind: "yoga";
150
+ yoga: string;
151
+ planets: string[];
152
+ }
153
+ export type FactAtom = PlacementAtom | AspectAtom | PatternAtom | SignatureAtom | AngleAtom | DispositorAtom | ReceptionAtom | StarAtom | LotAtom | TransitAtom | SynastryAtom | CompositeAtom | TimelordAtom | DignityAtom | NakshatraAtom | VargaAtom | YogaAtom;
76
154
  /** A chart as a flat, ranked list of {@link FactAtom}s. */
77
155
  export interface InterpretationContext {
78
156
  jdUt: number;
@@ -108,6 +186,22 @@ export interface SalienceWeights {
108
186
  dispositor: number;
109
187
  /** Added to a mutual reception. */
110
188
  reception: number;
189
+ /** Added to a body's conjunction with a fixed star. */
190
+ star: number;
191
+ /** Added to a Hermetic lot (the Part of Fortune and its companions). */
192
+ lot: number;
193
+ /** Added to a transit-to-natal aspect. */
194
+ transit: number;
195
+ /** Added to a synastry aspect or house overlay. */
196
+ synastry: number;
197
+ /** Added to a composite midpoint placement. */
198
+ composite: number;
199
+ /** Added to an active time-lord period. */
200
+ timelord: number;
201
+ /** Added to a finer essential-dignity fact. */
202
+ dignityFine: number;
203
+ /** Added to a nakshatra / varga / yoga fact. */
204
+ vedic: number;
111
205
  }
112
206
  export declare const DEFAULT_SALIENCE: SalienceWeights;
113
207
  export interface ContextOptions {
@@ -122,6 +216,64 @@ export interface ContextOptions {
122
216
  realm?: Realm;
123
217
  certainty?: Certainty;
124
218
  };
219
+ /** Fixed-star conjunctions to project as `star` atoms. The engine does not
220
+ * compute these from a bare {@link Chart} (the star catalog lives in the
221
+ * data pack), so a caller supplies them, e.g. from
222
+ * {@link Engine.starConjunctions}. */
223
+ stars?: {
224
+ body: string;
225
+ star: string;
226
+ orb: number;
227
+ }[];
228
+ /** Hermetic lots to project as `lot` atoms, e.g. from {@link Engine.lots}. */
229
+ lots?: {
230
+ lot: string;
231
+ sign: string;
232
+ signDeg: number;
233
+ house: number;
234
+ }[];
235
+ /** Transit-to-natal hits, e.g. from {@link transitAspects}. */
236
+ transits?: TransitHit[];
237
+ /** Synastry aspects and/or house overlays between two charts. */
238
+ synastry?: {
239
+ aspects?: SynastryAspectHit[];
240
+ overlays?: SynastryOverlays;
241
+ };
242
+ /** Composite midpoint placements, e.g. from {@link compositePlacements}. */
243
+ composite?: CompositePlacement[];
244
+ /** Active time-lord periods at a target instant (caller-supplied). */
245
+ timelords?: {
246
+ profection?: Profection;
247
+ zr?: {
248
+ l1: string;
249
+ l2: string;
250
+ l3: string;
251
+ l4: string;
252
+ lot?: string;
253
+ };
254
+ firdaria?: {
255
+ major: string | null;
256
+ sub: string | null;
257
+ day?: boolean;
258
+ };
259
+ dasha?: {
260
+ maha: string;
261
+ antar?: string | null;
262
+ pratyantar?: string | null;
263
+ moon_nakshatra?: string;
264
+ };
265
+ };
266
+ /** Vedic structure facts (caller-supplied or auto from sidereal chart). */
267
+ vedic?: {
268
+ /** Project nakshatras for these bodies from the chart longitudes. */
269
+ nakshatraBodies?: string[];
270
+ /** Project varga D-n for these bodies (default `[9]` when set true). */
271
+ vargas?: number[] | true;
272
+ yogas?: {
273
+ yoga: string;
274
+ planets: string[];
275
+ }[];
276
+ };
125
277
  }
126
278
  /**
127
279
  * Project a {@link Chart} into a ranked list of {@link FactAtom}s -- the
@@ -23,7 +23,9 @@ import { mod } from "./core.js";
23
23
  import { SIGNS, DOMICILE, EXALTATION } from "./chart.js";
24
24
  import { detectPatterns } from "./patterns.js";
25
25
  import { chartSignature } from "./signature.js";
26
- import { TRIPLICITY } from "./dignity-score.js";
26
+ import { TRIPLICITY, dignityScore, almuten } from "./dignity-score.js";
27
+ import { nakshatra } from "./vedic.js";
28
+ import { varga } from "./vargas.js";
27
29
  const LUMINARIES = new Set(["sun", "moon"]);
28
30
  const ANGULAR_HOUSES = new Set([1, 4, 7, 10]);
29
31
  const HARD_ASPECTS = new Set(["conjunction", "square", "opposition"]);
@@ -53,6 +55,8 @@ const DIGNITY_RANK = { domicile: 3, exaltation: 2, triplicity: 1 };
53
55
  export const DEFAULT_SALIENCE = {
54
56
  base: 1, luminary: 1.5, angular: 1, chartRuler: 1,
55
57
  dignity: 0.5, hardAspect: 1, pattern: 4, dispositor: 0.5, reception: 2,
58
+ star: 2, lot: 2, transit: 1.5, synastry: 1, composite: 0.8, timelord: 2,
59
+ dignityFine: 0.4, vedic: 1,
56
60
  };
57
61
  /** How much to keep of a time-sensitive atom's salience at each certainty -- the
58
62
  * Moon and the angles move fastest, so an uncertain instant trusts them least. */
@@ -62,7 +66,7 @@ const TIME_SENSITIVE_KEEP = {
62
66
  /** Time-sensitive atoms: the angles (rotate ~15°/h) and anything about the Moon
63
67
  * (~13°/day), the fastest-shifting facts under a time error. */
64
68
  function timeSensitive(atom) {
65
- return atom.kind === "angle" || atom.bodies.includes("moon");
69
+ return atom.kind === "angle" || atom.kind === "lot" || atom.bodies.includes("moon");
66
70
  }
67
71
  function title(body) {
68
72
  return body.split("_").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
@@ -234,6 +238,237 @@ export function interpretationContext(chart, opts = {}) {
234
238
  angleAtom("mc", chart.angles.mc);
235
239
  angleAtom("vertex", chart.angles.vertex);
236
240
  angleAtom("eastPoint", chart.angles.eastPoint);
241
+ // Fixed-star conjunctions (caller-supplied; the catalog is not on the Chart).
242
+ for (const sc of opts.stars ?? []) {
243
+ let salience = w.base + w.star;
244
+ if (LUMINARIES.has(sc.body))
245
+ salience += w.luminary;
246
+ atoms.push({
247
+ id: `star:${sc.body}:${sc.star}`, kind: "star", bodies: [sc.body], salience,
248
+ body: sc.body, star: sc.star, orb: sc.orb,
249
+ text: `${title(sc.body)} conjunct ${sc.star} (orb ${sc.orb.toFixed(1)}°)`,
250
+ });
251
+ }
252
+ // Hermetic lots (caller-supplied; computed from the chart's points + sect).
253
+ for (const l of opts.lots ?? []) {
254
+ atoms.push({
255
+ id: `lot:${l.lot}`, kind: "lot", bodies: [], salience: w.base + w.lot,
256
+ lot: l.lot, sign: l.sign, signDeg: l.signDeg, house: l.house,
257
+ text: `Lot of ${title(l.lot)} in ${l.sign}, house ${l.house}`,
258
+ });
259
+ }
260
+ // Finer essential dignities: term, face, triplicity held, almuten of each degree.
261
+ const chartSect = sunHouse !== undefined && sunHouse >= 7 ? "day" : "night";
262
+ for (const body of CLASSICAL) {
263
+ const p = chart.bodies[body];
264
+ if (!p)
265
+ continue;
266
+ const ds = dignityScore(body, p.lon, chartSect);
267
+ const alm = almuten(p.lon, chartSect);
268
+ let sal = w.base + w.dignityFine;
269
+ if (LUMINARIES.has(body))
270
+ sal += w.luminary;
271
+ atoms.push({
272
+ id: `term:${body}:${ds.term_ruler}`, kind: "dignity", bodies: [body], salience: sal,
273
+ facet: "term", body, ruler: ds.term_ruler,
274
+ text: `${title(body)} in the term of ${title(ds.term_ruler)}`
275
+ + (ds.term > 0 ? " (holds term dignity)" : ""),
276
+ });
277
+ atoms.push({
278
+ id: `face:${body}:${ds.face_ruler}`, kind: "dignity", bodies: [body], salience: sal,
279
+ facet: "face", body, ruler: ds.face_ruler,
280
+ text: `${title(body)} in the face of ${title(ds.face_ruler)}`
281
+ + (ds.face > 0 ? " (holds face dignity)" : ""),
282
+ });
283
+ if (ds.triplicity > 0) {
284
+ atoms.push({
285
+ id: `triplicity:${body}`, kind: "dignity", bodies: [body],
286
+ salience: sal + w.dignity, facet: "triplicity", body,
287
+ text: `${title(body)} holds ${chartSect} triplicity`,
288
+ });
289
+ }
290
+ atoms.push({
291
+ id: `almuten:${body}:${alm.planet}`, kind: "dignity", bodies: [body],
292
+ salience: sal + (alm.planet === body ? w.dignity : 0),
293
+ facet: "almuten", body, ruler: alm.planet,
294
+ text: `${title(alm.planet)} is almuten of ${title(body)}'s degree`,
295
+ });
296
+ }
297
+ // Transit-to-natal aspects (caller-supplied).
298
+ for (const t of opts.transits ?? []) {
299
+ let salience = w.base + w.transit + t.strength;
300
+ if (HARD_ASPECTS.has(t.aspect))
301
+ salience += w.hardAspect;
302
+ if (LUMINARIES.has(t.transit) || LUMINARIES.has(t.natal))
303
+ salience += w.luminary;
304
+ atoms.push({
305
+ id: `transit:${t.transit}~natal_${t.natal}:${t.aspect}`, kind: "transit",
306
+ bodies: [t.transit, t.natal], salience,
307
+ transit: t.transit, natal: t.natal, aspect: t.aspect, orb: t.orb,
308
+ phase: t.phase, strength: t.strength, natalHouse: t.natalHouse,
309
+ text: `Transiting ${title(t.transit)} ${t.aspect} natal ${title(t.natal)} `
310
+ + `(${t.phase}, orb ${t.orb.toFixed(1)}°, natal house ${t.natalHouse})`,
311
+ });
312
+ }
313
+ // Synastry: inter-chart aspects and house overlays.
314
+ for (const s of opts.synastry?.aspects ?? []) {
315
+ let salience = w.base + w.synastry + s.strength;
316
+ if (HARD_ASPECTS.has(s.aspect))
317
+ salience += w.hardAspect;
318
+ if (LUMINARIES.has(s.a) || LUMINARIES.has(s.b))
319
+ salience += w.luminary;
320
+ atoms.push({
321
+ id: `synastry:${s.a}~b_${s.b}:${s.aspect}`, kind: "synastry",
322
+ bodies: [s.a, s.b], salience, mode: "aspect",
323
+ a: s.a, b: s.b, aspect: s.aspect, orb: s.orb, strength: s.strength,
324
+ text: `${title(s.a)} ${s.aspect} partner's ${title(s.b)} (orb ${s.orb.toFixed(1)}°)`,
325
+ });
326
+ }
327
+ const overlays = opts.synastry?.overlays;
328
+ if (overlays) {
329
+ for (const [body, house] of Object.entries(overlays.aInB)) {
330
+ atoms.push({
331
+ id: `synastry:overlay:a:${body}:house:${house}`, kind: "synastry",
332
+ bodies: [body], salience: w.base + w.synastry, mode: "overlay",
333
+ body, partner: "a", house,
334
+ text: `${title(body)} falls in partner's house ${house}`,
335
+ });
336
+ }
337
+ for (const [body, house] of Object.entries(overlays.bInA)) {
338
+ atoms.push({
339
+ id: `synastry:overlay:b:${body}:house:${house}`, kind: "synastry",
340
+ bodies: [body], salience: w.base + w.synastry, mode: "overlay",
341
+ body, partner: "b", house,
342
+ text: `Partner's ${title(body)} falls in house ${house}`,
343
+ });
344
+ }
345
+ }
346
+ // Composite midpoint placements.
347
+ for (const c of opts.composite ?? []) {
348
+ atoms.push({
349
+ id: `composite:${c.body}`, kind: "composite", bodies: [c.body],
350
+ salience: w.base + w.composite + (LUMINARIES.has(c.body) ? w.luminary : 0),
351
+ body: c.body, sign: c.sign, signDeg: c.signDeg,
352
+ text: `Composite ${title(c.body)} in ${c.sign}`,
353
+ });
354
+ }
355
+ // Time-lords: profection, zodiacal releasing, firdaria, dasha.
356
+ const tl = opts.timelords;
357
+ if (tl?.profection) {
358
+ const pf = tl.profection;
359
+ atoms.push({
360
+ id: `profection:year:${pf.annual.sign.toLowerCase()}:${pf.annual.lord}`, kind: "timelord",
361
+ bodies: [pf.annual.lord], salience: w.base + w.timelord, system: "profection",
362
+ level: "year", lord: pf.annual.lord, sign: pf.annual.sign,
363
+ text: `Annual profection: ${pf.annual.sign} (house ${pf.annual.house}), lord ${title(pf.annual.lord)}`,
364
+ });
365
+ atoms.push({
366
+ id: `profection:month:${pf.monthly.sign.toLowerCase()}:${pf.monthly.lord}`, kind: "timelord",
367
+ bodies: [pf.monthly.lord], salience: w.base + w.timelord * 0.7, system: "profection",
368
+ level: "month", lord: pf.monthly.lord, sign: pf.monthly.sign,
369
+ text: `Monthly profection: ${pf.monthly.sign} (house ${pf.monthly.house}), lord ${title(pf.monthly.lord)}`,
370
+ });
371
+ }
372
+ if (tl?.zr) {
373
+ const zrWeight = { l1: 1, l2: 0.75, l3: 0.5, l4: 0.35 };
374
+ const zrLevels = [
375
+ ["l1", tl.zr.l1], ["l2", tl.zr.l2], ["l3", tl.zr.l3], ["l4", tl.zr.l4],
376
+ ];
377
+ for (const [level, sign] of zrLevels) {
378
+ const signIdx = SIGNS.indexOf(sign);
379
+ const lord = signIdx >= 0 ? SIGN_RULER[signIdx] : "";
380
+ atoms.push({
381
+ id: `zr:${level}:${sign.toLowerCase()}:${lord}`, kind: "timelord",
382
+ bodies: lord ? [lord] : [], salience: w.base + w.timelord * (zrWeight[level] ?? 0.5),
383
+ system: "zr", level, lord, sign,
384
+ text: `Zodiacal releasing ${level.toUpperCase()}: ${sign}`
385
+ + (lord ? `, lord ${title(lord)}` : "")
386
+ + (tl.zr.lot ? ` (from Lot of ${title(tl.zr.lot)})` : ""),
387
+ });
388
+ }
389
+ }
390
+ if (tl?.firdaria?.major) {
391
+ atoms.push({
392
+ id: `firdaria:major:${tl.firdaria.major}`, kind: "timelord",
393
+ bodies: [tl.firdaria.major], salience: w.base + w.timelord, system: "firdaria",
394
+ level: "major", lord: tl.firdaria.major,
395
+ text: `Firdaria major period: ${title(tl.firdaria.major)}`
396
+ + (tl.firdaria.day !== undefined ? ` (${tl.firdaria.day ? "day" : "night"} chart)` : ""),
397
+ });
398
+ if (tl.firdaria.sub) {
399
+ atoms.push({
400
+ id: `firdaria:sub:${tl.firdaria.sub}`, kind: "timelord",
401
+ bodies: [tl.firdaria.sub], salience: w.base + w.timelord * 0.7, system: "firdaria",
402
+ level: "sub", lord: tl.firdaria.sub,
403
+ text: `Firdaria sub-period: ${title(tl.firdaria.sub)}`,
404
+ });
405
+ }
406
+ }
407
+ if (tl?.dasha?.maha) {
408
+ const d = tl.dasha;
409
+ atoms.push({
410
+ id: `dasha:maha:${d.maha}`, kind: "timelord", bodies: [d.maha],
411
+ salience: w.base + w.timelord, system: "dasha", level: "maha", lord: d.maha,
412
+ text: `Vimshottari mahadasha: ${title(d.maha)}`
413
+ + (d.moon_nakshatra ? ` (Moon in ${d.moon_nakshatra})` : ""),
414
+ });
415
+ if (d.antar) {
416
+ atoms.push({
417
+ id: `dasha:antar:${d.antar}`, kind: "timelord", bodies: [d.antar],
418
+ salience: w.base + w.timelord * 0.8, system: "dasha", level: "antar", lord: d.antar,
419
+ text: `Vimshottari antardasha: ${title(d.antar)}`,
420
+ });
421
+ }
422
+ if (d.pratyantar) {
423
+ atoms.push({
424
+ id: `dasha:pratyantar:${d.pratyantar}`, kind: "timelord", bodies: [d.pratyantar],
425
+ salience: w.base + w.timelord * 0.6, system: "dasha", level: "pratyantar", lord: d.pratyantar,
426
+ text: `Vimshottari pratyantardasha: ${title(d.pratyantar)}`,
427
+ });
428
+ }
429
+ }
430
+ // Vedic: nakshatras, vargas, yogas.
431
+ const vedic = opts.vedic;
432
+ if (vedic) {
433
+ const nakBodies = vedic.nakshatraBodies
434
+ ?? ["moon", "sun", "mars", "mercury", "jupiter", "venus", "saturn"];
435
+ for (const body of nakBodies) {
436
+ const p = chart.bodies[body];
437
+ if (!p)
438
+ continue;
439
+ const nak = nakshatra(p.lon);
440
+ let salience = w.base + w.vedic;
441
+ if (body === "moon")
442
+ salience += w.luminary;
443
+ atoms.push({
444
+ id: `nakshatra:${body}:${nak.name.replace(/\s+/g, "_")}`, kind: "nakshatra",
445
+ bodies: [body], salience, body, name: nak.name, pada: nak.pada, lord: nak.lord,
446
+ text: `${title(body)} in ${nak.name} (pada ${nak.pada}, lord ${title(nak.lord)})`,
447
+ });
448
+ }
449
+ const vargaDivs = vedic.vargas === true ? [9] : (vedic.vargas ?? []);
450
+ for (const n of vargaDivs) {
451
+ for (const body of nakBodies) {
452
+ const p = chart.bodies[body];
453
+ if (!p)
454
+ continue;
455
+ const v = varga(p.lon, n);
456
+ atoms.push({
457
+ id: `varga:d${n}:${body}:${v.sign.toLowerCase()}`, kind: "varga",
458
+ bodies: [body], salience: w.base + w.vedic, division: n, body, sign: v.sign,
459
+ text: `${title(body)} D${n} (${v.sign})`,
460
+ });
461
+ }
462
+ }
463
+ for (const y of vedic.yogas ?? []) {
464
+ atoms.push({
465
+ id: `yoga:${y.yoga.replace(/\s+/g, "_")}`, kind: "yoga",
466
+ bodies: y.planets, salience: w.base + w.timelord * 0.5 + w.vedic,
467
+ yoga: y.yoga, planets: y.planets,
468
+ text: `Yoga ${y.yoga} (${y.planets.map(title).join(", ")})`,
469
+ });
470
+ }
471
+ }
237
472
  // An inexact instant trusts the fast-moving facts least.
238
473
  const prov = opts.provenance;
239
474
  if (prov?.certainty && prov.certainty !== "exact") {
@@ -0,0 +1,54 @@
1
+ import { type BodyId, type Chart, type Engine, type Zodiac } from "./chart.js";
2
+ import { type AspectPhase } from "./electional.js";
3
+ /** A transiting body aspecting a natal point. */
4
+ export interface TransitHit {
5
+ transit: string;
6
+ natal: string;
7
+ aspect: string;
8
+ orb: number;
9
+ phase: AspectPhase;
10
+ strength: number;
11
+ /** Natal house the transiting body occupies (by natal cusps). */
12
+ natalHouse: number;
13
+ }
14
+ /** An inter-chart aspect between person A's body and person B's body. */
15
+ export interface SynastryAspectHit {
16
+ a: string;
17
+ b: string;
18
+ aspect: string;
19
+ orb: number;
20
+ strength: number;
21
+ }
22
+ export interface SynastryOverlays {
23
+ /** Person A's bodies in person B's houses. */
24
+ aInB: Record<string, number>;
25
+ /** Person B's bodies in person A's houses. */
26
+ bInA: Record<string, number>;
27
+ }
28
+ /** A composite-chart body placement (midpoint method). */
29
+ export interface CompositePlacement {
30
+ body: string;
31
+ lon: number;
32
+ sign: string;
33
+ signDeg: number;
34
+ }
35
+ /**
36
+ * Transiting bodies aspecting a natal chart at `transitJd`. Natal longitudes are
37
+ * fixed; phase reflects the transiting body's motion toward the natal point.
38
+ */
39
+ export declare function transitAspects(natal: Chart, engine: Engine, transitJd: number, opts?: {
40
+ maxOrb?: number;
41
+ zodiac?: Zodiac;
42
+ orbs?: Record<string, number>;
43
+ bodies?: BodyId[];
44
+ }): TransitHit[];
45
+ /**
46
+ * Inter-chart aspects between two natal charts (static snapshot; both speeds 0).
47
+ */
48
+ export declare function synastryAspects(chartA: Chart, chartB: Chart, maxOrb?: number, orbs?: Record<string, number>): SynastryAspectHit[];
49
+ /** House overlays both ways between two charts. */
50
+ export declare function synastryOverlays(chartA: Chart, chartB: Chart): SynastryOverlays;
51
+ /**
52
+ * Midpoint-composite placements for `bodies` from two birth instants.
53
+ */
54
+ export declare function compositePlacements(engine: Engine, jdA: number, jdB: number, bodies?: BodyId[], zodiac?: Zodiac): CompositePlacement[];
@@ -0,0 +1,103 @@
1
+ /**
2
+ * astroengine relational -- diachronic and two-chart derivations for the
3
+ * interpretation layer: transits vs natal, synastry, composite midpoints.
4
+ *
5
+ * Pure geometry on validated positions; no interpretation. Mirrors the MCP
6
+ * transits/synastry/composite tools but returns structured hits the fact
7
+ * projection can turn into citable atoms.
8
+ */
9
+ import { mod } from "./core.js";
10
+ import { ASPECTS, BODIES, DEFAULT_ORBS, NOT_ASPECTABLE, SIGNS, } from "./chart.js";
11
+ import { aspectPhase } from "./electional.js";
12
+ import { compositeLongitudes } from "./derived.js";
13
+ function houseIndex(lon, cusps) {
14
+ for (let i = 0; i < 12; i++) {
15
+ if (mod(lon - cusps[i], 360) < mod(cusps[(i + 1) % 12] - cusps[i], 360))
16
+ return i + 1;
17
+ }
18
+ return 12;
19
+ }
20
+ function aspectHits(lonA, speedA, labelA, lonB, speedB, labelB, maxOrb, orbs) {
21
+ const sep = Math.abs(mod(lonA - lonB + 180, 360) - 180);
22
+ const out = [];
23
+ for (const [name, angle] of Object.entries(ASPECTS)) {
24
+ const limit = Math.min(maxOrb, orbs[name] ?? maxOrb);
25
+ const orb = Math.abs(sep - angle);
26
+ if (orb > limit)
27
+ continue;
28
+ const orbRounded = Math.round(orb * 100) / 100;
29
+ out.push({
30
+ a: labelA, b: labelB, aspect: name, orb: orbRounded,
31
+ phase: aspectPhase(lonA, speedA, lonB, speedB, angle),
32
+ strength: Math.max(0, 1 - orbRounded / limit),
33
+ });
34
+ }
35
+ return out;
36
+ }
37
+ /**
38
+ * Transiting bodies aspecting a natal chart at `transitJd`. Natal longitudes are
39
+ * fixed; phase reflects the transiting body's motion toward the natal point.
40
+ */
41
+ export function transitAspects(natal, engine, transitJd, opts = {}) {
42
+ const maxOrb = opts.maxOrb ?? 3;
43
+ const orbs = opts.orbs ?? DEFAULT_ORBS;
44
+ const zodiac = opts.zodiac ?? natal.zodiac;
45
+ const bodies = opts.bodies ?? BODIES;
46
+ const natalBodies = bodies.filter((b) => natal.bodies[b] && !NOT_ASPECTABLE.has(b));
47
+ const out = [];
48
+ for (const tb of bodies) {
49
+ if (NOT_ASPECTABLE.has(tb))
50
+ continue;
51
+ const tp = engine.position(tb, transitJd, { zodiac });
52
+ const natalHouse = houseIndex(tp.lon, natal.cusps);
53
+ for (const nb of natalBodies) {
54
+ const nLon = natal.bodies[nb].lon;
55
+ for (const hit of aspectHits(tp.lon, tp.speed, tb, nLon, 0, nb, maxOrb, orbs)) {
56
+ out.push({
57
+ transit: hit.a, natal: hit.b, aspect: hit.aspect,
58
+ orb: hit.orb, phase: hit.phase, strength: hit.strength, natalHouse,
59
+ });
60
+ }
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ /**
66
+ * Inter-chart aspects between two natal charts (static snapshot; both speeds 0).
67
+ */
68
+ export function synastryAspects(chartA, chartB, maxOrb = 4, orbs = DEFAULT_ORBS) {
69
+ const bodies = BODIES.filter((b) => chartA.bodies[b] && chartB.bodies[b] && !NOT_ASPECTABLE.has(b));
70
+ const out = [];
71
+ for (const ba of bodies) {
72
+ const la = chartA.bodies[ba].lon;
73
+ for (const bb of bodies) {
74
+ const lb = chartB.bodies[bb].lon;
75
+ for (const hit of aspectHits(la, 0, ba, lb, 0, bb, maxOrb, orbs)) {
76
+ out.push({ a: hit.a, b: hit.b, aspect: hit.aspect, orb: hit.orb, strength: hit.strength });
77
+ }
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ /** House overlays both ways between two charts. */
83
+ export function synastryOverlays(chartA, chartB) {
84
+ const bodies = BODIES.filter((b) => chartA.bodies[b] && chartB.bodies[b]);
85
+ const aInB = {};
86
+ const bInA = {};
87
+ for (const b of bodies) {
88
+ aInB[b] = houseIndex(chartA.bodies[b].lon, chartB.cusps);
89
+ bInA[b] = houseIndex(chartB.bodies[b].lon, chartA.cusps);
90
+ }
91
+ return { aInB, bInA };
92
+ }
93
+ /**
94
+ * Midpoint-composite placements for `bodies` from two birth instants.
95
+ */
96
+ export function compositePlacements(engine, jdA, jdB, bodies = BODIES, zodiac = "tropical") {
97
+ const lons = compositeLongitudes(engine, jdA, jdB, bodies, zodiac);
98
+ return bodies.map((body) => {
99
+ const lon = mod(lons[body], 360);
100
+ const signIdx = Math.floor(lon / 30) % 12;
101
+ return { body, lon, sign: SIGNS[signIdx], signDeg: mod(lon, 30) };
102
+ });
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caelus",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Astrological ephemeris engine. MIT, no AGPL, no ephemeris files. Checked against Swiss Ephemeris.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",