caelus 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/accuracy.json +15 -15
- package/dist/src/anchored.d.ts +49 -0
- package/dist/src/anchored.js +40 -0
- package/dist/src/astrocartography.js +6 -1
- package/dist/src/brief.d.ts +90 -0
- package/dist/src/brief.js +85 -0
- package/dist/src/chart.d.ts +5 -0
- package/dist/src/chart.js +20 -4
- package/dist/src/counterfactual.d.ts +82 -0
- package/dist/src/counterfactual.js +105 -0
- package/dist/src/eclipses.d.ts +103 -0
- package/dist/src/eclipses.js +260 -2
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +6 -0
- package/dist/src/interpret.d.ts +156 -0
- package/dist/src/interpret.js +179 -0
- package/dist/src/interpretation.d.ts +137 -0
- package/dist/src/interpretation.js +250 -0
- package/dist/src/node-loader.js +5 -2
- package/dist/src/parans.d.ts +28 -0
- package/dist/src/parans.js +84 -0
- package/dist/src/provenance.d.ts +135 -0
- package/dist/src/provenance.js +159 -0
- package/package.json +1 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const hit = (atoms) => ({ matched: atoms.length > 0, atoms });
|
|
2
|
+
// ------------------------------------------------------------- atom selectors
|
|
3
|
+
/** Matches placement atoms by any subset of body / sign / house / retrograde /
|
|
4
|
+
* a held dignity. */
|
|
5
|
+
export function hasPlacement(filter = {}) {
|
|
6
|
+
return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "placement"
|
|
7
|
+
&& (filter.body === undefined || a.body === filter.body)
|
|
8
|
+
&& (filter.sign === undefined || a.sign === filter.sign)
|
|
9
|
+
&& (filter.house === undefined || a.house === filter.house)
|
|
10
|
+
&& (filter.retrograde === undefined || a.retrograde === filter.retrograde)
|
|
11
|
+
&& (filter.dignity === undefined || a.dignities.includes(filter.dignity))));
|
|
12
|
+
}
|
|
13
|
+
/** Matches aspect atoms. `between` is an unordered pair; `minStrength` filters
|
|
14
|
+
* loose aspects; `phase` filters applying/separating. */
|
|
15
|
+
export function hasAspect(filter = {}) {
|
|
16
|
+
const pair = filter.between ? [...filter.between].sort() : null;
|
|
17
|
+
return (ctx) => hit(ctx.atoms.filter((at) => {
|
|
18
|
+
if (at.kind !== "aspect")
|
|
19
|
+
return false;
|
|
20
|
+
if (filter.a !== undefined && at.a !== filter.a)
|
|
21
|
+
return false;
|
|
22
|
+
if (filter.b !== undefined && at.b !== filter.b)
|
|
23
|
+
return false;
|
|
24
|
+
if (pair && [at.a, at.b].sort().join() !== pair.join())
|
|
25
|
+
return false;
|
|
26
|
+
if (filter.aspect !== undefined && at.aspect !== filter.aspect)
|
|
27
|
+
return false;
|
|
28
|
+
if (filter.phase !== undefined && at.phase !== filter.phase)
|
|
29
|
+
return false;
|
|
30
|
+
if (filter.minStrength !== undefined && at.strength < filter.minStrength)
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
/** Matches configuration atoms by kind and/or a participating body. */
|
|
36
|
+
export function hasPattern(filter = {}) {
|
|
37
|
+
return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "pattern"
|
|
38
|
+
&& (filter.kind === undefined || a.pattern === filter.kind)
|
|
39
|
+
&& (filter.body === undefined || a.bodies.includes(filter.body))));
|
|
40
|
+
}
|
|
41
|
+
/** Matches a structural-signature facet, e.g. `("element", "fire")`. */
|
|
42
|
+
export function hasSignature(facet, value) {
|
|
43
|
+
return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "signature" && a.facet === facet
|
|
44
|
+
&& (value === undefined || a.value === value)));
|
|
45
|
+
}
|
|
46
|
+
/** Matches an angle atom by which angle and/or its sign. */
|
|
47
|
+
export function hasAngle(angle, sign) {
|
|
48
|
+
return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "angle" && a.angle === angle
|
|
49
|
+
&& (sign === undefined || a.sign === sign)));
|
|
50
|
+
}
|
|
51
|
+
/** Matches dispositor atoms by body, its dispositor, and/or the final flag
|
|
52
|
+
* (a body in its own domicile that terminates a dispositor chain). */
|
|
53
|
+
export function hasDispositor(filter = {}) {
|
|
54
|
+
return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "dispositor"
|
|
55
|
+
&& (filter.body === undefined || a.body === filter.body)
|
|
56
|
+
&& (filter.dispositor === undefined || a.dispositor === filter.dispositor)
|
|
57
|
+
&& (filter.final === undefined || a.final === filter.final)));
|
|
58
|
+
}
|
|
59
|
+
/** Matches a mutual reception, optionally involving a given body. */
|
|
60
|
+
export function hasReception(filter = {}) {
|
|
61
|
+
return (ctx) => hit(ctx.atoms.filter((a) => a.kind === "reception"
|
|
62
|
+
&& (filter.body === undefined || a.bodies.includes(filter.body))));
|
|
63
|
+
}
|
|
64
|
+
// ----------------------------------------------------------------- combinators
|
|
65
|
+
/** Matches only when every selector matches; returns the union of their atoms. */
|
|
66
|
+
export function matchAll(...sels) {
|
|
67
|
+
return (ctx) => {
|
|
68
|
+
const parts = sels.map((s) => s(ctx));
|
|
69
|
+
return parts.every((p) => p.matched)
|
|
70
|
+
? { matched: true, atoms: dedupe(parts.flatMap((p) => p.atoms)) }
|
|
71
|
+
: { matched: false, atoms: [] };
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Matches when any selector matches; returns the atoms from those that did. */
|
|
75
|
+
export function matchAny(...sels) {
|
|
76
|
+
return (ctx) => {
|
|
77
|
+
const matched = sels.map((s) => s(ctx)).filter((p) => p.matched);
|
|
78
|
+
return matched.length
|
|
79
|
+
? { matched: true, atoms: dedupe(matched.flatMap((p) => p.atoms)) }
|
|
80
|
+
: { matched: false, atoms: [] };
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Matches when the selector does NOT match (an absence test); no atoms. */
|
|
84
|
+
export function matchNone(sel) {
|
|
85
|
+
return (ctx) => ({ matched: !sel(ctx).matched, atoms: [] });
|
|
86
|
+
}
|
|
87
|
+
function dedupe(atoms) {
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
return atoms.filter((a) => (seen.has(a.id) ? false : seen.add(a.id)));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run interpretation sources against a fact projection and return a ranked
|
|
93
|
+
* {@link Reading}. Each rule whose selector matches emits an entry carrying the
|
|
94
|
+
* matched atom ids (provenance) and a salience = sum of those atoms' salience x
|
|
95
|
+
* the rule weight. The engine never ships the content: the sources are the
|
|
96
|
+
* caller's.
|
|
97
|
+
*
|
|
98
|
+
* @param ctx A projection from {@link interpretationContext}.
|
|
99
|
+
* @param sources One or more {@link InterpretationSource} corpora.
|
|
100
|
+
* @returns The {@link Reading}; entries are sorted by descending salience.
|
|
101
|
+
*/
|
|
102
|
+
export function interpret(ctx, sources) {
|
|
103
|
+
const entries = [];
|
|
104
|
+
for (const src of sources) {
|
|
105
|
+
for (const rule of src.rules) {
|
|
106
|
+
const m = rule.when(ctx);
|
|
107
|
+
if (!m.matched)
|
|
108
|
+
continue;
|
|
109
|
+
const text = typeof rule.text === "function" ? rule.text(m, ctx) : rule.text;
|
|
110
|
+
const salience = m.atoms.reduce((s, a) => s + a.salience, 0) * (rule.weight ?? 1);
|
|
111
|
+
entries.push({
|
|
112
|
+
id: `${src.id}/${rule.id}`, source: src.id, rule: rule.id, text,
|
|
113
|
+
atomIds: m.atoms.map((a) => a.id), salience, tags: rule.tags,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
entries.sort((p, q) => q.salience - p.salience || (p.id < q.id ? -1 : 1));
|
|
118
|
+
return { jdUt: ctx.jdUt, entries };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Group a {@link Reading}'s entries by the facts they share, so statements about
|
|
122
|
+
* the same atoms surface together rather than scattered through a flat list --
|
|
123
|
+
* the substrate for "everything said about this placement" and for spotting
|
|
124
|
+
* contention. Entries are connected when their cited atoms overlap; an entry
|
|
125
|
+
* citing nothing (an absence rule) stands alone. A group is `contested` when a
|
|
126
|
+
* declared conflicting tag-pair both appear in it.
|
|
127
|
+
*
|
|
128
|
+
* Semantic contradiction is the corpus author's to declare (via `tags` +
|
|
129
|
+
* `conflicts`); the resolver does the bookkeeping, not the judgement.
|
|
130
|
+
*
|
|
131
|
+
* @param reading A reading from {@link interpret}.
|
|
132
|
+
* @param opts Conflicting tag pairs and optional text de-duplication.
|
|
133
|
+
* @returns Groups sorted by descending salience.
|
|
134
|
+
*/
|
|
135
|
+
export function reconcile(reading, opts = {}) {
|
|
136
|
+
let entries = reading.entries;
|
|
137
|
+
if (opts.dedupe) {
|
|
138
|
+
const seen = new Set(); // entries arrive salience-sorted: keep first
|
|
139
|
+
entries = entries.filter((e) => (seen.has(e.text) ? false : seen.add(e.text)));
|
|
140
|
+
}
|
|
141
|
+
// Union-find over entries that share an atom id.
|
|
142
|
+
const parent = entries.map((_, i) => i);
|
|
143
|
+
const find = (x) => {
|
|
144
|
+
while (parent[x] !== x) {
|
|
145
|
+
parent[x] = parent[parent[x]];
|
|
146
|
+
x = parent[x];
|
|
147
|
+
}
|
|
148
|
+
return x;
|
|
149
|
+
};
|
|
150
|
+
const firstByAtom = new Map();
|
|
151
|
+
entries.forEach((e, i) => {
|
|
152
|
+
for (const id of e.atomIds) {
|
|
153
|
+
const seen = firstByAtom.get(id);
|
|
154
|
+
if (seen === undefined)
|
|
155
|
+
firstByAtom.set(id, i);
|
|
156
|
+
else
|
|
157
|
+
parent[find(i)] = find(seen);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const buckets = new Map();
|
|
161
|
+
entries.forEach((e, i) => {
|
|
162
|
+
const r = find(i);
|
|
163
|
+
(buckets.get(r) ?? buckets.set(r, []).get(r)).push(e);
|
|
164
|
+
});
|
|
165
|
+
const conflicts = opts.conflicts ?? [];
|
|
166
|
+
const groups = [...buckets.values()].map((es) => {
|
|
167
|
+
es.sort((a, b) => b.salience - a.salience || (a.id < b.id ? -1 : 1));
|
|
168
|
+
const tags = [...new Set(es.flatMap((e) => e.tags ?? []))];
|
|
169
|
+
return {
|
|
170
|
+
atomIds: [...new Set(es.flatMap((e) => e.atomIds))],
|
|
171
|
+
entries: es, tags,
|
|
172
|
+
contested: conflicts.some(([x, y]) => tags.includes(x) && tags.includes(y)),
|
|
173
|
+
salience: Math.max(...es.map((e) => e.salience)),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
groups.sort((a, b) => b.salience - a.salience
|
|
177
|
+
|| (a.atomIds.join() < b.atomIds.join() ? -1 : 1));
|
|
178
|
+
return groups;
|
|
179
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Chart, Zodiac } from "./chart.js";
|
|
2
|
+
import type { AspectPhase } from "./electional.js";
|
|
3
|
+
import { ChartPattern } from "./patterns.js";
|
|
4
|
+
import { ChartSignature } from "./signature.js";
|
|
5
|
+
import type { Realm, Certainty } from "./provenance.js";
|
|
6
|
+
/** Atom kinds in an {@link InterpretationContext}. */
|
|
7
|
+
export type FactKind = "placement" | "aspect" | "pattern" | "signature" | "angle" | "dispositor" | "reception";
|
|
8
|
+
interface FactAtomBase {
|
|
9
|
+
/** Stable, content-addressable id, e.g. `"placement:mars"` or
|
|
10
|
+
* `"aspect:mars~saturn:square"`. Interpretations cite this. */
|
|
11
|
+
id: string;
|
|
12
|
+
kind: FactKind;
|
|
13
|
+
/** Body ids this atom concerns (empty for body-less signature facets). */
|
|
14
|
+
bodies: string[];
|
|
15
|
+
/** Transparent salience (higher = more prominent); see {@link SalienceWeights}. */
|
|
16
|
+
salience: number;
|
|
17
|
+
/** Plain-language statement of the fact -- no interpretation. */
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PlacementAtom extends FactAtomBase {
|
|
21
|
+
kind: "placement";
|
|
22
|
+
body: string;
|
|
23
|
+
sign: string;
|
|
24
|
+
signDeg: number;
|
|
25
|
+
house: number;
|
|
26
|
+
retrograde: boolean;
|
|
27
|
+
dignities: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface AspectAtom extends FactAtomBase {
|
|
30
|
+
kind: "aspect";
|
|
31
|
+
a: string;
|
|
32
|
+
b: string;
|
|
33
|
+
aspect: string;
|
|
34
|
+
/** Orb from exact, degrees. */
|
|
35
|
+
orb: number;
|
|
36
|
+
/** Applying, separating, or exact -- from the two bodies' speeds. */
|
|
37
|
+
phase: AspectPhase;
|
|
38
|
+
/** Closeness in `[0, 1]`: `1` exact, `0` at the orb limit. */
|
|
39
|
+
strength: number;
|
|
40
|
+
}
|
|
41
|
+
export interface PatternAtom extends FactAtomBase {
|
|
42
|
+
kind: "pattern";
|
|
43
|
+
/** Configuration kind, e.g. `"t_square"`, `"grand_trine"`. */
|
|
44
|
+
pattern: string;
|
|
45
|
+
/** Focal body for a T-square or yod. */
|
|
46
|
+
apex?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface SignatureAtom extends FactAtomBase {
|
|
49
|
+
kind: "signature";
|
|
50
|
+
/** Which facet of the structural signature this states. */
|
|
51
|
+
facet: "element" | "modality" | "sign" | "ruler";
|
|
52
|
+
value: string;
|
|
53
|
+
}
|
|
54
|
+
export interface AngleAtom extends FactAtomBase {
|
|
55
|
+
kind: "angle";
|
|
56
|
+
angle: "asc" | "mc" | "vertex" | "eastPoint";
|
|
57
|
+
sign: string;
|
|
58
|
+
signDeg: number;
|
|
59
|
+
}
|
|
60
|
+
export interface DispositorAtom extends FactAtomBase {
|
|
61
|
+
kind: "dispositor";
|
|
62
|
+
body: string;
|
|
63
|
+
/** The classical ruler of the body's sign (equals `body` when in domicile). */
|
|
64
|
+
dispositor: string;
|
|
65
|
+
/** The body occupies its own domicile -- a chain terminus / final dispositor. */
|
|
66
|
+
final: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface ReceptionAtom extends FactAtomBase {
|
|
69
|
+
kind: "reception";
|
|
70
|
+
/** The dignities the reception runs through: a single dignity when both
|
|
71
|
+
* bodies receive by the same (`"domicile"`, `"exaltation"`, `"triplicity"`),
|
|
72
|
+
* else a sorted pair for a mixed reception (e.g. `"domicile-exaltation"`). */
|
|
73
|
+
by: string;
|
|
74
|
+
}
|
|
75
|
+
export type FactAtom = PlacementAtom | AspectAtom | PatternAtom | SignatureAtom | AngleAtom | DispositorAtom | ReceptionAtom;
|
|
76
|
+
/** A chart as a flat, ranked list of {@link FactAtom}s. */
|
|
77
|
+
export interface InterpretationContext {
|
|
78
|
+
jdUt: number;
|
|
79
|
+
zodiac: Zodiac;
|
|
80
|
+
/** Atoms sorted by descending {@link FactAtomBase.salience}, then `id`. */
|
|
81
|
+
atoms: FactAtom[];
|
|
82
|
+
/** What the chart is, when supplied via {@link ContextOptions.provenance} --
|
|
83
|
+
* framing for an interpreter (a forecast is provisional, a mythic chart is a
|
|
84
|
+
* symbol, not a biography). */
|
|
85
|
+
realm?: Realm;
|
|
86
|
+
/** How firmly the instant is known. When not `"exact"`, time-sensitive atoms
|
|
87
|
+
* (the Moon, the angles) are damped, since their positions are less certain. */
|
|
88
|
+
certainty?: Certainty;
|
|
89
|
+
}
|
|
90
|
+
/** Additive salience weights. Each contribution is documented at its use site;
|
|
91
|
+
* override any subset through {@link ContextOptions.salience}. */
|
|
92
|
+
export interface SalienceWeights {
|
|
93
|
+
/** Every atom starts here. */
|
|
94
|
+
base: number;
|
|
95
|
+
/** Added when the Sun or Moon is involved. */
|
|
96
|
+
luminary: number;
|
|
97
|
+
/** Added for an angular house (1/4/7/10) or an angle atom. */
|
|
98
|
+
angular: number;
|
|
99
|
+
/** Added to the placement of the Ascendant ruler. */
|
|
100
|
+
chartRuler: number;
|
|
101
|
+
/** Added per essential dignity a body holds. */
|
|
102
|
+
dignity: number;
|
|
103
|
+
/** Added to a hard aspect (conjunction/square/opposition). */
|
|
104
|
+
hardAspect: number;
|
|
105
|
+
/** Base salience of a whole configuration (T-square, grand trine, ...). */
|
|
106
|
+
pattern: number;
|
|
107
|
+
/** Added to a dispositor link (and again when it is a final dispositor). */
|
|
108
|
+
dispositor: number;
|
|
109
|
+
/** Added to a mutual reception. */
|
|
110
|
+
reception: number;
|
|
111
|
+
}
|
|
112
|
+
export declare const DEFAULT_SALIENCE: SalienceWeights;
|
|
113
|
+
export interface ContextOptions {
|
|
114
|
+
/** Salience weights to override (merged over {@link DEFAULT_SALIENCE}). */
|
|
115
|
+
salience?: Partial<SalienceWeights>;
|
|
116
|
+
/** Precomputed patterns/signature, to avoid recomputing them. */
|
|
117
|
+
patterns?: ChartPattern[];
|
|
118
|
+
signature?: ChartSignature;
|
|
119
|
+
/** The chart's grounding. Carried onto the context; an inexact `certainty`
|
|
120
|
+
* damps time-sensitive atoms. Wire from {@link realize}'s result. */
|
|
121
|
+
provenance?: {
|
|
122
|
+
realm?: Realm;
|
|
123
|
+
certainty?: Certainty;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Project a {@link Chart} into a ranked list of {@link FactAtom}s -- the
|
|
128
|
+
* substrate an interpretation layer consumes. Pure and deterministic; computes
|
|
129
|
+
* applying/separating and a normalized strength for each aspect that the bare
|
|
130
|
+
* {@link Chart.aspects} list omits.
|
|
131
|
+
*
|
|
132
|
+
* @param chart A chart from {@link Engine.chart} / {@link Engine.chartAt}.
|
|
133
|
+
* @param opts Salience overrides, orb policy, and precomputed reductions.
|
|
134
|
+
* @returns The {@link InterpretationContext}; `atoms` are sorted by salience.
|
|
135
|
+
*/
|
|
136
|
+
export declare function interpretationContext(chart: Chart, opts?: ContextOptions): InterpretationContext;
|
|
137
|
+
export {};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine interpretation context -- a chart projected into typed, addressable
|
|
3
|
+
* "fact atoms" for an interpretation layer to consume.
|
|
4
|
+
*
|
|
5
|
+
* Caelus stops at validated geometry; this is the seam where interpretation
|
|
6
|
+
* begins. It does NOT interpret. It normalizes a {@link Chart}'s facts --
|
|
7
|
+
* placements, aspects, classical configurations, the structural signature, the
|
|
8
|
+
* angles -- into a flat list of atoms, each with a stable id, a transparent
|
|
9
|
+
* salience score, and a plain-language rendering. Rule-based and LLM-based
|
|
10
|
+
* interpreters alike build on this substrate: a rule corpus matches atoms by
|
|
11
|
+
* `kind`/`id`, and an LLM reads the `text` and cites the `id`.
|
|
12
|
+
*
|
|
13
|
+
* Salience is explicit and overridable (see {@link SalienceWeights}), never a
|
|
14
|
+
* magic number -- it ranks atoms so a reader can lead with what is prominent
|
|
15
|
+
* (luminaries, angular placements, the chart ruler, tight and hard aspects,
|
|
16
|
+
* whole configurations) without the engine asserting meaning.
|
|
17
|
+
*
|
|
18
|
+
* This is TS-side framework code, not ephemeris: there is no Swiss Ephemeris
|
|
19
|
+
* oracle for "which facts matter," so it is unit-tested for structure rather
|
|
20
|
+
* than pinned by a parity golden.
|
|
21
|
+
*/
|
|
22
|
+
import { mod } from "./core.js";
|
|
23
|
+
import { SIGNS, DOMICILE, EXALTATION } from "./chart.js";
|
|
24
|
+
import { detectPatterns } from "./patterns.js";
|
|
25
|
+
import { chartSignature } from "./signature.js";
|
|
26
|
+
import { TRIPLICITY } from "./dignity-score.js";
|
|
27
|
+
const LUMINARIES = new Set(["sun", "moon"]);
|
|
28
|
+
const ANGULAR_HOUSES = new Set([1, 4, 7, 10]);
|
|
29
|
+
const HARD_ASPECTS = new Set(["conjunction", "square", "opposition"]);
|
|
30
|
+
/** The seven classical planets that participate in the dispositor scheme. */
|
|
31
|
+
const CLASSICAL = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn"];
|
|
32
|
+
/** Traditional domicile ruler of each sign (index 0-11), inverted from
|
|
33
|
+
* {@link DOMICILE}: the body that rules a body's sign disposits it. */
|
|
34
|
+
const SIGN_RULER = (() => {
|
|
35
|
+
const r = new Array(12);
|
|
36
|
+
for (const [body, signs] of Object.entries(DOMICILE)) {
|
|
37
|
+
for (const s of signs)
|
|
38
|
+
r[s] = body;
|
|
39
|
+
}
|
|
40
|
+
return r;
|
|
41
|
+
})();
|
|
42
|
+
/** Body exalted in each sign (index 0-11), or undefined; inverted from
|
|
43
|
+
* {@link EXALTATION}. */
|
|
44
|
+
const SIGN_EXALT = (() => {
|
|
45
|
+
const r = new Array(12);
|
|
46
|
+
for (const [body, sign] of Object.entries(EXALTATION))
|
|
47
|
+
r[sign] = body;
|
|
48
|
+
return r;
|
|
49
|
+
})();
|
|
50
|
+
/** How a reception's dignities rank (stronger = larger), for the salience
|
|
51
|
+
* scaling and the `by` summary. */
|
|
52
|
+
const DIGNITY_RANK = { domicile: 3, exaltation: 2, triplicity: 1 };
|
|
53
|
+
export const DEFAULT_SALIENCE = {
|
|
54
|
+
base: 1, luminary: 1.5, angular: 1, chartRuler: 1,
|
|
55
|
+
dignity: 0.5, hardAspect: 1, pattern: 4, dispositor: 0.5, reception: 2,
|
|
56
|
+
};
|
|
57
|
+
/** How much to keep of a time-sensitive atom's salience at each certainty -- the
|
|
58
|
+
* Moon and the angles move fastest, so an uncertain instant trusts them least. */
|
|
59
|
+
const TIME_SENSITIVE_KEEP = {
|
|
60
|
+
exact: 1, approximate: 0.7, representative: 0.6, none: 0.5,
|
|
61
|
+
};
|
|
62
|
+
/** Time-sensitive atoms: the angles (rotate ~15°/h) and anything about the Moon
|
|
63
|
+
* (~13°/day), the fastest-shifting facts under a time error. */
|
|
64
|
+
function timeSensitive(atom) {
|
|
65
|
+
return atom.kind === "angle" || atom.bodies.includes("moon");
|
|
66
|
+
}
|
|
67
|
+
function title(body) {
|
|
68
|
+
return body.split("_").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
69
|
+
}
|
|
70
|
+
function humanizePattern(kind) {
|
|
71
|
+
const special = {
|
|
72
|
+
t_square: "T-square", grand_trine: "Grand trine", grand_cross: "Grand cross",
|
|
73
|
+
mystic_rectangle: "Mystic rectangle", stellium_sign: "Stellium",
|
|
74
|
+
stellium_house: "Stellium",
|
|
75
|
+
};
|
|
76
|
+
return special[kind] ?? title(kind);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Project a {@link Chart} into a ranked list of {@link FactAtom}s -- the
|
|
80
|
+
* substrate an interpretation layer consumes. Pure and deterministic; computes
|
|
81
|
+
* applying/separating and a normalized strength for each aspect that the bare
|
|
82
|
+
* {@link Chart.aspects} list omits.
|
|
83
|
+
*
|
|
84
|
+
* @param chart A chart from {@link Engine.chart} / {@link Engine.chartAt}.
|
|
85
|
+
* @param opts Salience overrides, orb policy, and precomputed reductions.
|
|
86
|
+
* @returns The {@link InterpretationContext}; `atoms` are sorted by salience.
|
|
87
|
+
*/
|
|
88
|
+
export function interpretationContext(chart, opts = {}) {
|
|
89
|
+
const w = { ...DEFAULT_SALIENCE, ...opts.salience };
|
|
90
|
+
const sig = opts.signature ?? chartSignature(chart);
|
|
91
|
+
const patterns = opts.patterns ?? detectPatterns(chart);
|
|
92
|
+
const atoms = [];
|
|
93
|
+
// Placements: one atom per present body.
|
|
94
|
+
for (const [body, p] of Object.entries(chart.bodies)) {
|
|
95
|
+
if (!p)
|
|
96
|
+
continue;
|
|
97
|
+
let salience = w.base;
|
|
98
|
+
if (LUMINARIES.has(body))
|
|
99
|
+
salience += w.luminary;
|
|
100
|
+
if (ANGULAR_HOUSES.has(p.house))
|
|
101
|
+
salience += w.angular;
|
|
102
|
+
if (sig.ruler === body)
|
|
103
|
+
salience += w.chartRuler;
|
|
104
|
+
salience += w.dignity * p.dignities.length;
|
|
105
|
+
const extra = [
|
|
106
|
+
p.retrograde ? "retrograde" : null,
|
|
107
|
+
...p.dignities,
|
|
108
|
+
].filter(Boolean);
|
|
109
|
+
atoms.push({
|
|
110
|
+
id: `placement:${body}`, kind: "placement", bodies: [body], salience,
|
|
111
|
+
body, sign: p.sign, signDeg: p.signDeg, house: p.house,
|
|
112
|
+
retrograde: p.retrograde, dignities: p.dignities,
|
|
113
|
+
text: `${title(body)} in ${p.sign}, house ${p.house}`
|
|
114
|
+
+ (extra.length ? ` (${extra.join(", ")})` : ""),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// Aspects: phase and strength come straight from the enriched chart aspect.
|
|
118
|
+
for (const asp of chart.aspects) {
|
|
119
|
+
let salience = w.base + asp.strength;
|
|
120
|
+
if (HARD_ASPECTS.has(asp.aspect))
|
|
121
|
+
salience += w.hardAspect;
|
|
122
|
+
if (LUMINARIES.has(asp.a) || LUMINARIES.has(asp.b))
|
|
123
|
+
salience += w.luminary;
|
|
124
|
+
const [x, y] = [asp.a, asp.b].sort();
|
|
125
|
+
atoms.push({
|
|
126
|
+
id: `aspect:${x}~${y}:${asp.aspect}`, kind: "aspect", bodies: [asp.a, asp.b],
|
|
127
|
+
salience, a: asp.a, b: asp.b, aspect: asp.aspect, orb: asp.orb,
|
|
128
|
+
phase: asp.phase, strength: asp.strength,
|
|
129
|
+
text: `${title(asp.a)} ${asp.aspect} ${title(asp.b)} `
|
|
130
|
+
+ `(${asp.phase}, orb ${Math.abs(asp.orb).toFixed(1)}°)`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Configurations.
|
|
134
|
+
for (const pat of patterns) {
|
|
135
|
+
let salience = w.pattern;
|
|
136
|
+
if (pat.bodies.some((b) => LUMINARIES.has(b)))
|
|
137
|
+
salience += w.luminary;
|
|
138
|
+
const names = pat.bodies.map(title).join(", ");
|
|
139
|
+
atoms.push({
|
|
140
|
+
id: `pattern:${pat.kind}:${pat.bodies.join("-")}`, kind: "pattern",
|
|
141
|
+
bodies: pat.bodies, salience, pattern: pat.kind, apex: pat.apex,
|
|
142
|
+
text: `${humanizePattern(pat.kind)}: ${names}`
|
|
143
|
+
+ (pat.apex ? ` (apex ${title(pat.apex)})` : "")
|
|
144
|
+
+ (pat.sign ? ` in ${pat.sign}` : ""),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Structural signature: the dominant facets and the chart ruler.
|
|
148
|
+
const sigAtom = (facet, value, text) => {
|
|
149
|
+
if (value === null)
|
|
150
|
+
return;
|
|
151
|
+
atoms.push({
|
|
152
|
+
id: `signature:${facet}:${value}`, kind: "signature",
|
|
153
|
+
bodies: facet === "ruler" ? [value] : [], salience: w.base + 1,
|
|
154
|
+
facet, value, text,
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
sigAtom("element", sig.dominant.element, `${title(sig.dominant.element)} is the dominant element`);
|
|
158
|
+
sigAtom("modality", sig.dominant.modality, `${title(sig.dominant.modality)} is the dominant modality`);
|
|
159
|
+
sigAtom("sign", sig.dominant.sign, `${sig.dominant.sign} is the most-occupied sign`);
|
|
160
|
+
sigAtom("ruler", sig.ruler, `${title(sig.ruler ?? "")} is the chart ruler`);
|
|
161
|
+
// Dispositors: the classical ruler of each classical planet's sign, plus any
|
|
162
|
+
// mutual receptions (a disposits b and b disposits a) among them.
|
|
163
|
+
const dispositorOf = (body) => {
|
|
164
|
+
const p = chart.bodies[body];
|
|
165
|
+
return p ? SIGN_RULER[Math.floor(mod(p.lon, 360) / 30)] : null;
|
|
166
|
+
};
|
|
167
|
+
for (const body of CLASSICAL) {
|
|
168
|
+
if (!chart.bodies[body])
|
|
169
|
+
continue;
|
|
170
|
+
const disp = dispositorOf(body);
|
|
171
|
+
const final = disp === body;
|
|
172
|
+
let salience = w.base + w.dispositor + (final ? w.dispositor : 0);
|
|
173
|
+
if (LUMINARIES.has(body))
|
|
174
|
+
salience += w.luminary;
|
|
175
|
+
atoms.push({
|
|
176
|
+
id: `dispositor:${body}`, kind: "dispositor", bodies: [body], salience,
|
|
177
|
+
body, dispositor: disp, final,
|
|
178
|
+
text: final
|
|
179
|
+
? `${title(body)} is in its own domicile (final dispositor)`
|
|
180
|
+
: `${title(body)} is disposited by ${title(disp)}`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Reception (mutual): each body holds a dignity in the other's sign. Checked
|
|
184
|
+
// by domicile, exaltation, and the sect's triplicity ruler (sect = day when
|
|
185
|
+
// the Sun is above the horizon, houses 7-12). `by` names the strongest
|
|
186
|
+
// dignity each direction; salience scales with the weaker link.
|
|
187
|
+
const sunHouse = chart.bodies.sun?.house;
|
|
188
|
+
const sect = sunHouse !== undefined && sunHouse >= 7 ? 0 : 1; // 0 day, 1 night
|
|
189
|
+
const signOf = (body) => Math.floor(mod(chart.bodies[body].lon, 360) / 30);
|
|
190
|
+
const receives = (a, otherSign) => {
|
|
191
|
+
const ds = [];
|
|
192
|
+
if (SIGN_RULER[otherSign] === a)
|
|
193
|
+
ds.push("domicile");
|
|
194
|
+
if (SIGN_EXALT[otherSign] === a)
|
|
195
|
+
ds.push("exaltation");
|
|
196
|
+
if (TRIPLICITY[otherSign % 4][sect] === a)
|
|
197
|
+
ds.push("triplicity");
|
|
198
|
+
return ds;
|
|
199
|
+
};
|
|
200
|
+
const strongest = (ds) => ds.reduce((best, d) => (DIGNITY_RANK[d] > DIGNITY_RANK[best] ? d : best), ds[0]);
|
|
201
|
+
for (let i = 0; i < CLASSICAL.length; i++) {
|
|
202
|
+
for (let j = i + 1; j < CLASSICAL.length; j++) {
|
|
203
|
+
const a = CLASSICAL[i];
|
|
204
|
+
const b = CLASSICAL[j];
|
|
205
|
+
if (!chart.bodies[a] || !chart.bodies[b])
|
|
206
|
+
continue;
|
|
207
|
+
const aRec = receives(a, signOf(b));
|
|
208
|
+
const bRec = receives(b, signOf(a));
|
|
209
|
+
if (!aRec.length || !bRec.length)
|
|
210
|
+
continue;
|
|
211
|
+
const da = strongest(aRec);
|
|
212
|
+
const db = strongest(bRec);
|
|
213
|
+
const by = da === db ? da : [da, db].sort().join("-");
|
|
214
|
+
let salience = w.base + w.reception * (Math.min(DIGNITY_RANK[da], DIGNITY_RANK[db]) / 3);
|
|
215
|
+
if (LUMINARIES.has(a) || LUMINARIES.has(b))
|
|
216
|
+
salience += w.luminary;
|
|
217
|
+
atoms.push({
|
|
218
|
+
id: `reception:${a}~${b}`, kind: "reception", bodies: [a, b], salience, by,
|
|
219
|
+
text: `Mutual reception: ${title(a)} and ${title(b)} (${by})`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Angles.
|
|
224
|
+
const angleAtom = (angle, lon) => {
|
|
225
|
+
const sign = SIGNS[Math.floor(mod(lon, 360) / 30)];
|
|
226
|
+
const label = { asc: "Ascendant", mc: "Midheaven", vertex: "Vertex", eastPoint: "East Point" }[angle];
|
|
227
|
+
atoms.push({
|
|
228
|
+
id: `angle:${angle}`, kind: "angle", bodies: [], salience: w.base + w.angular,
|
|
229
|
+
angle, sign, signDeg: mod(lon, 30),
|
|
230
|
+
text: `${label} in ${sign}`,
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
angleAtom("asc", chart.angles.asc);
|
|
234
|
+
angleAtom("mc", chart.angles.mc);
|
|
235
|
+
angleAtom("vertex", chart.angles.vertex);
|
|
236
|
+
angleAtom("eastPoint", chart.angles.eastPoint);
|
|
237
|
+
// An inexact instant trusts the fast-moving facts least.
|
|
238
|
+
const prov = opts.provenance;
|
|
239
|
+
if (prov?.certainty && prov.certainty !== "exact") {
|
|
240
|
+
const keep = TIME_SENSITIVE_KEEP[prov.certainty];
|
|
241
|
+
for (const a of atoms)
|
|
242
|
+
if (timeSensitive(a))
|
|
243
|
+
a.salience *= keep;
|
|
244
|
+
}
|
|
245
|
+
atoms.sort((m, n) => n.salience - m.salience || (m.id < n.id ? -1 : 1));
|
|
246
|
+
return {
|
|
247
|
+
jdUt: chart.jdUt, zodiac: chart.zodiac, atoms,
|
|
248
|
+
realm: prov?.realm, certainty: prov?.certainty,
|
|
249
|
+
};
|
|
250
|
+
}
|
package/dist/src/node-loader.js
CHANGED
|
@@ -34,8 +34,11 @@ export function loadNodeData(dir, level = "embedded", moonTier = "full") {
|
|
|
34
34
|
if (existsSync(join(dir, "fixed_stars.json"))) {
|
|
35
35
|
data.fixedStars = j("fixed_stars.json");
|
|
36
36
|
}
|
|
37
|
-
// asteroid packs (Horizons fits): loaded when present, ~380 KB total
|
|
38
|
-
|
|
37
|
+
// asteroid packs (Horizons fits): loaded when present, ~380 KB total.
|
|
38
|
+
// `pluto` is optional too: when a wide-range Chebyshev pack is present it
|
|
39
|
+
// supersedes the embedded Meeus ch.37 series (valid 1885-2099) above, so
|
|
40
|
+
// Pluto extends past that window at full precision; see fit_pluto.py.
|
|
41
|
+
for (const b of ["ceres", "pallas", "juno", "vesta", "pholus", "pluto"]) {
|
|
39
42
|
if (existsSync(join(dir, `${b}_cheb.json`))) {
|
|
40
43
|
(data.chebPacks ??= {})[b] = j(`${b}_cheb.json`);
|
|
41
44
|
}
|
package/dist/src/parans.d.ts
CHANGED
|
@@ -25,3 +25,31 @@ export interface Paran {
|
|
|
25
25
|
* @returns The co-angular pairs as {@link Paran} objects.
|
|
26
26
|
*/
|
|
27
27
|
export declare function parans(engine: Engine, jd: number, lat: number, bodies?: string[], toleranceMin?: number): Paran[];
|
|
28
|
+
/**
|
|
29
|
+
* The four angle crossings of a fixed `star` over the day from `jd` at latitude
|
|
30
|
+
* `lat`: the meridian transits always occur; `rise`/`set` are absent when the
|
|
31
|
+
* star is circumpolar or never rises.
|
|
32
|
+
*/
|
|
33
|
+
export declare function starAngleTimes(engine: Engine, star: string, jd: number, lat: number): Record<string, number>;
|
|
34
|
+
/** One star-to-body paran: a fixed star and a body on angles at ~the same time. */
|
|
35
|
+
export interface StarParan {
|
|
36
|
+
star: string;
|
|
37
|
+
star_angle: string;
|
|
38
|
+
body: string;
|
|
39
|
+
body_angle: string;
|
|
40
|
+
jd: number;
|
|
41
|
+
gap_min: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Star-to-body parans over the day from `jd` (UT) at latitude `lat`: a fixed
|
|
45
|
+
* star and a moving body simultaneously on angles within `toleranceMin`
|
|
46
|
+
* minutes — Brady's fixed-star parans. Ordered by (star, body, jd).
|
|
47
|
+
*
|
|
48
|
+
* @param engine An engine whose data pack includes the fixed-star catalog.
|
|
49
|
+
* @param jd Julian Day in UT (the 24-hour window starts here).
|
|
50
|
+
* @param lat Geographic latitude in degrees, north positive.
|
|
51
|
+
* @param stars Catalog star names to test (see {@link Engine.starNames}).
|
|
52
|
+
* @param bodies Bodies to consider; defaults to the seven classical planets.
|
|
53
|
+
* @param toleranceMin The paran window in minutes (default 30).
|
|
54
|
+
*/
|
|
55
|
+
export declare function starParans(engine: Engine, jd: number, lat: number, stars: string[], bodies?: string[], toleranceMin?: number): StarParan[];
|