caelus 0.17.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/provenance.d.ts +135 -0
- package/dist/src/provenance.js +159 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ ephemeris files. 1:1 port of the Python reference, checked by golden fixtures.
|
|
|
5
5
|
|
|
6
6
|
## Verification chain
|
|
7
7
|
|
|
8
|
-
1. Python engine checked against Swiss Ephemeris 2.10 across
|
|
8
|
+
1. Python engine checked against Swiss Ephemeris 2.10 across 1850–2150:
|
|
9
9
|
every planet ≤ 1″ (Sun–Saturn), Moon ≤ 2.5″, Chiron ≤ 1″, mean node ≤ 1″,
|
|
10
10
|
true node ≤ 1′ vs SE's built-in ephemeris (≤ 1″ vs JPL DE431)
|
|
11
11
|
(vs full DE431 files, 1850–2149), angles and Placidus cusps ≤ 3.2″ — all
|
|
@@ -100,4 +100,4 @@ test/golden.test.ts conformance suite vs Python fixtures
|
|
|
100
100
|
- caelus — this package
|
|
101
101
|
- [caelus-birth](https://www.npmjs.com/package/caelus-birth) — local birth time + place → UT (charts take UT; use this)
|
|
102
102
|
- [caelus-wheel](https://www.npmjs.com/package/caelus-wheel) — React SVG chart wheel
|
|
103
|
-
- [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server, twenty-
|
|
103
|
+
- [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server, twenty-nine chart tools over stdio
|
package/accuracy.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"basis": "Swiss Ephemeris 2.10,
|
|
3
|
-
"range": "
|
|
2
|
+
"basis": "Swiss Ephemeris 2.10, 1850-2150, apparent geocentric ecliptic longitude (true equinox of date)",
|
|
3
|
+
"range": "1850-2150",
|
|
4
4
|
"unit": "arcsec",
|
|
5
5
|
"bodies": [
|
|
6
6
|
{
|
|
7
7
|
"name": "Sun",
|
|
8
|
-
"max": "0.
|
|
8
|
+
"max": "0.5",
|
|
9
9
|
"rms": "0.2",
|
|
10
10
|
"note": ""
|
|
11
11
|
},
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"name": "Mercury",
|
|
26
|
-
"max": "0.
|
|
26
|
+
"max": "0.6",
|
|
27
27
|
"rms": "0.2",
|
|
28
28
|
"note": ""
|
|
29
29
|
},
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"name": "Saturn",
|
|
50
|
-
"max": "0
|
|
50
|
+
"max": "1.0",
|
|
51
51
|
"rms": "0.4",
|
|
52
52
|
"note": ""
|
|
53
53
|
},
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
"name": "Pluto",
|
|
68
|
-
"max": "
|
|
68
|
+
"max": "3.4",
|
|
69
69
|
"rms": "1.0",
|
|
70
|
-
"note": "series
|
|
70
|
+
"note": "JPL Horizons barycenter Chebyshev pack (1700-2212, fit residual 4.1e-6 AU ≈ 0.03″); supersedes the Meeus ch.37 series. Bound is vs SE's own Moshier Pluto"
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"name": "Chiron",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"name": "Mean Lilith",
|
|
104
|
-
"max": "1.
|
|
104
|
+
"max": "1.6",
|
|
105
105
|
"rms": "0.5",
|
|
106
106
|
"note": "mean lunar apogee on the inclined orbit; latitude ≤0.1″"
|
|
107
107
|
},
|
|
@@ -113,13 +113,13 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "RA / Dec",
|
|
116
|
-
"max": "2.
|
|
116
|
+
"max": "2.6",
|
|
117
117
|
"rms": "—",
|
|
118
118
|
"note": "rotation is exact; bound tracks each body's ecliptic accuracy (Moon worst)"
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
"name": "Topocentric Moon",
|
|
122
|
-
"max": "2.
|
|
122
|
+
"max": "2.7",
|
|
123
123
|
"rms": "—",
|
|
124
124
|
"note": "parallax model adds ≤0.1″ over the geocentric bound"
|
|
125
125
|
},
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"name": "True Lilith (osc. apogee)",
|
|
164
|
-
"max": "
|
|
164
|
+
"max": "214",
|
|
165
165
|
"rms": "—",
|
|
166
166
|
"note": "hypersensitive to the lunar theory (~1/e amplification); SE's own Moshier-vs-DE difference dominates. 'True Lilith' values disagree across software at this scale"
|
|
167
167
|
},
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
},
|
|
222
222
|
{
|
|
223
223
|
"label": "Pluto / Chiron",
|
|
224
|
-
"bound": "≤
|
|
224
|
+
"bound": "≤ 3.4″ / ≤ 1″"
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
227
|
"label": "Angles & Placidus cusps",
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
},
|
|
234
234
|
{
|
|
235
235
|
"label": "Mean Lilith",
|
|
236
|
-
"bound": "≤ 1.
|
|
236
|
+
"bound": "≤ 1.6″"
|
|
237
237
|
},
|
|
238
238
|
{
|
|
239
239
|
"label": "8 new house systems",
|
|
@@ -268,12 +268,12 @@
|
|
|
268
268
|
"bound": "types exact; max ≤ 9 s"
|
|
269
269
|
}
|
|
270
270
|
],
|
|
271
|
-
"v03_harness": "python/validate_swiss.py regenerates every figure above the line against pyswisseph 2.10 (Moshier mode)",
|
|
271
|
+
"v03_harness": "python/validate_swiss.py regenerates every figure above the line against pyswisseph 2.10 (Moshier mode), sampled over 1850-2150; validate_horizons.py cross-checks the edges (banded 1800/2200) against JPL directly",
|
|
272
272
|
"true_node_vs_builtin": "1′",
|
|
273
273
|
"counts": {
|
|
274
274
|
"house_systems": 12,
|
|
275
275
|
"sidereal_ayanamsas": 7,
|
|
276
|
-
"mcp_tools":
|
|
276
|
+
"mcp_tools": 29,
|
|
277
277
|
"default_bodies": 13
|
|
278
278
|
}
|
|
279
279
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine anchored charts -- realize a {@link Realm} + anchors into a chart.
|
|
3
|
+
*
|
|
4
|
+
* This is where the provenance layer meets the two generators. Given what a
|
|
5
|
+
* chart *is* and how its time/place are anchored, {@link realize} routes:
|
|
6
|
+
* when an instant can be resolved it runs the ephemeris (`chartAt`); when it
|
|
7
|
+
* cannot but constraints are supplied it runs the compiler's symbolic synthesis
|
|
8
|
+
* (`compileForm`); otherwise it reports that nothing could be computed and why.
|
|
9
|
+
* The realm rides along as framing for the interpretation layer.
|
|
10
|
+
*/
|
|
11
|
+
import { Engine, Chart, ChartOptions } from "./chart.js";
|
|
12
|
+
import { CompiledForm, Constraint } from "./compiler.js";
|
|
13
|
+
import { Realm, TemporalAnchor, SpatialAnchor, AnchorRegistry, ResolvedTime, ResolvedPlace } from "./provenance.js";
|
|
14
|
+
/** A chart described by its realm and anchors, not a bare (instant, place). */
|
|
15
|
+
export interface AnchoredChart {
|
|
16
|
+
realm: Realm;
|
|
17
|
+
when: TemporalAnchor;
|
|
18
|
+
where?: SpatialAnchor;
|
|
19
|
+
/** Constraints for the compiler path -- a chart with no instant
|
|
20
|
+
* (archetypal / conceptual / symbolic). */
|
|
21
|
+
constraints?: Constraint[];
|
|
22
|
+
}
|
|
23
|
+
/** The outcome of {@link realize}: how (or whether) a chart was produced. */
|
|
24
|
+
export interface RealizedChart {
|
|
25
|
+
realm: Realm;
|
|
26
|
+
/** `ephemeris` (an instant resolved), `compiler` (synthesized from
|
|
27
|
+
* constraints), or `none` (neither was possible). */
|
|
28
|
+
via: "ephemeris" | "compiler" | "none";
|
|
29
|
+
time: ResolvedTime;
|
|
30
|
+
place: ResolvedPlace;
|
|
31
|
+
/** The computed chart, when an instant was available. */
|
|
32
|
+
chart: Chart | null;
|
|
33
|
+
/** The synthesized form, when the compiler path ran. */
|
|
34
|
+
form: CompiledForm | null;
|
|
35
|
+
note: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Realize an {@link AnchoredChart}: resolve its anchors and run the appropriate
|
|
39
|
+
* generator. A resolvable instant always wins (the realm is then just framing);
|
|
40
|
+
* failing that, constraints synthesize a form; failing both, nothing computes.
|
|
41
|
+
*
|
|
42
|
+
* @param engine The engine for the ephemeris path.
|
|
43
|
+
* @param anchored The realm + temporal/spatial anchors (+ optional constraints).
|
|
44
|
+
* @param registry Lookups for relative/narrative/named anchors.
|
|
45
|
+
* @param opts Chart options (house system, zodiac) for the ephemeris path.
|
|
46
|
+
* @returns A {@link RealizedChart}; `chart`/`form` are null on the paths that
|
|
47
|
+
* did not run.
|
|
48
|
+
*/
|
|
49
|
+
export declare function realize(engine: Engine, anchored: AnchoredChart, registry?: AnchorRegistry, opts?: ChartOptions): RealizedChart;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { compileForm } from "./compiler.js";
|
|
2
|
+
import { resolveTime, resolvePlace, isTimeAnchored, } from "./provenance.js";
|
|
3
|
+
/**
|
|
4
|
+
* Realize an {@link AnchoredChart}: resolve its anchors and run the appropriate
|
|
5
|
+
* generator. A resolvable instant always wins (the realm is then just framing);
|
|
6
|
+
* failing that, constraints synthesize a form; failing both, nothing computes.
|
|
7
|
+
*
|
|
8
|
+
* @param engine The engine for the ephemeris path.
|
|
9
|
+
* @param anchored The realm + temporal/spatial anchors (+ optional constraints).
|
|
10
|
+
* @param registry Lookups for relative/narrative/named anchors.
|
|
11
|
+
* @param opts Chart options (house system, zodiac) for the ephemeris path.
|
|
12
|
+
* @returns A {@link RealizedChart}; `chart`/`form` are null on the paths that
|
|
13
|
+
* did not run.
|
|
14
|
+
*/
|
|
15
|
+
export function realize(engine, anchored, registry = {}, opts = {}) {
|
|
16
|
+
const time = resolveTime(anchored.when, registry);
|
|
17
|
+
const place = resolvePlace(anchored.where ?? { kind: "none", reason: "intentionally_unset" }, registry);
|
|
18
|
+
const base = { realm: anchored.realm, time, place };
|
|
19
|
+
if (time.jd !== null) {
|
|
20
|
+
const chart = engine.chartAt(time.jd, place.place?.lat ?? 0, place.place?.lonEast ?? 0, opts);
|
|
21
|
+
return {
|
|
22
|
+
...base, via: "ephemeris", chart, form: null,
|
|
23
|
+
note: `ephemeris (${time.certainty}) `
|
|
24
|
+
+ (place.place ? `at ${place.note ?? "given coordinates"}` : "no place; houses nominal at 0,0"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (anchored.constraints?.length) {
|
|
28
|
+
const form = compileForm(anchored.constraints);
|
|
29
|
+
return {
|
|
30
|
+
...base, via: "compiler", chart: null, form,
|
|
31
|
+
note: `compiler synthesis (${form.impossible ? "impossible form" : `residual ${form.residual.toFixed(2)}`})`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...base, via: "none", chart: null, form: null,
|
|
36
|
+
note: isTimeAnchored(anchored.realm)
|
|
37
|
+
? `time-anchored realm but no instant resolved (${time.note ?? "no time"}); supply a resolvable anchor or constraints`
|
|
38
|
+
: `${anchored.realm} realm has no instant; supply constraints to synthesize a form`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -24,10 +24,15 @@ export function planetLines(ra, dec, gastDeg, latMin = -85.0, latMax = 85.0, lat
|
|
|
24
24
|
const asc = [];
|
|
25
25
|
const dsc = [];
|
|
26
26
|
const n = Math.floor((latMax - latMin) / latStep + 1e-9); // never exceed latMax
|
|
27
|
+
// Skip the degenerate near-tangent point where |x| -> 1 (h0 -> 0 or 180): the
|
|
28
|
+
// body grazes the horizon at the meridian, acos' is singular there, and
|
|
29
|
+
// cross-platform libm rounding would swing the longitude by ~mas. Trimming it
|
|
30
|
+
// costs <0.003 deg of line length and makes the track deterministic.
|
|
31
|
+
const EDGE = 1.0 - 1e-9;
|
|
27
32
|
for (let i = 0; i <= n; i++) {
|
|
28
33
|
const phi = latMin + i * latStep;
|
|
29
34
|
const x = -Math.tan(phi * DEG) * td;
|
|
30
|
-
if (x >= -
|
|
35
|
+
if (x >= -EDGE && x <= EDGE) {
|
|
31
36
|
const h0 = Math.acos(x) / DEG; // hour-angle half-width, degrees
|
|
32
37
|
asc.push([mapLon(ra - h0 - gastDeg), phi]); // eastern horizon
|
|
33
38
|
dsc.push([mapLon(ra + h0 - gastDeg), phi]); // western horizon
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine interpretation brief -- a chart as compact, citable LLM input,
|
|
3
|
+
* and an audit that the model cited real facts.
|
|
4
|
+
*
|
|
5
|
+
* This is the "novel and accurate" seam. An LLM writes fluent, original prose
|
|
6
|
+
* (novel); to keep it honest (accurate) it is given only the validated fact
|
|
7
|
+
* atoms, each tagged with a stable id, and asked to cite the id(s) every
|
|
8
|
+
* statement rests on. {@link auditCitations} then checks those citations
|
|
9
|
+
* resolve -- a claim that cites an id not in the brief invented its provenance
|
|
10
|
+
* and is flagged. The chart math was never the model's to hallucinate.
|
|
11
|
+
*
|
|
12
|
+
* Pairs with the MCP app, where the host model is already the interpreter:
|
|
13
|
+
* feed it {@link Brief.prompt} instead of raw positions it would guess at.
|
|
14
|
+
*/
|
|
15
|
+
import type { FactKind, InterpretationContext } from "./interpretation.js";
|
|
16
|
+
import type { Reading } from "./interpret.js";
|
|
17
|
+
import type { Zodiac } from "./chart.js";
|
|
18
|
+
import type { Realm, Certainty } from "./provenance.js";
|
|
19
|
+
/** Default instruction header prepended to {@link Brief.prompt}. */
|
|
20
|
+
export declare const BRIEF_INSTRUCTIONS: string;
|
|
21
|
+
/** A one-line framing for the realm and certainty, or `""` when neither needs it. */
|
|
22
|
+
export declare function realmFraming(realm?: Realm, certainty?: Certainty): string;
|
|
23
|
+
export interface BriefOptions {
|
|
24
|
+
/** Keep only the top-N facts by salience. Default: all. */
|
|
25
|
+
limit?: number;
|
|
26
|
+
/** Restrict to certain atom kinds. */
|
|
27
|
+
kinds?: FactKind[];
|
|
28
|
+
/** Drop facts below this salience. */
|
|
29
|
+
minSalience?: number;
|
|
30
|
+
/** Fold a resolved {@link Reading}'s entries in as suggested readings. */
|
|
31
|
+
reading?: Reading;
|
|
32
|
+
/** Prepend {@link BRIEF_INSTRUCTIONS}. Default `true`. */
|
|
33
|
+
header?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** A salience-ranked fact in a {@link Brief}. */
|
|
36
|
+
export interface BriefFact {
|
|
37
|
+
id: string;
|
|
38
|
+
kind: FactKind;
|
|
39
|
+
text: string;
|
|
40
|
+
salience: number;
|
|
41
|
+
}
|
|
42
|
+
/** A chart rendered as citable LLM input. */
|
|
43
|
+
export interface Brief {
|
|
44
|
+
jdUt: number;
|
|
45
|
+
zodiac: Zodiac;
|
|
46
|
+
/** The facts offered, ranked by salience. */
|
|
47
|
+
facts: BriefFact[];
|
|
48
|
+
/** A prompt-ready, id-tagged rendering of {@link Brief.facts}. */
|
|
49
|
+
prompt: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Render an {@link InterpretationContext} as a compact, id-tagged {@link Brief}
|
|
53
|
+
* for an LLM to interpret and cite.
|
|
54
|
+
*
|
|
55
|
+
* @param ctx A projection from {@link interpretationContext}.
|
|
56
|
+
* @param opts Capping, kind filter, an optional {@link Reading} to fold in, and
|
|
57
|
+
* whether to prepend {@link BRIEF_INSTRUCTIONS}.
|
|
58
|
+
* @returns The {@link Brief}: a ranked `facts` list and a ready `prompt`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function chartBrief(ctx: InterpretationContext, opts?: BriefOptions): Brief;
|
|
61
|
+
/** A model-produced statement and the fact ids it claims to rest on. */
|
|
62
|
+
export interface Claim {
|
|
63
|
+
text: string;
|
|
64
|
+
cites: string[];
|
|
65
|
+
}
|
|
66
|
+
/** The result of {@link auditCitations}. */
|
|
67
|
+
export interface CitationAudit {
|
|
68
|
+
/** True when every cited id resolves to a fact in the context. */
|
|
69
|
+
ok: boolean;
|
|
70
|
+
/** Total claims examined. */
|
|
71
|
+
claims: number;
|
|
72
|
+
/** Claims that cited at least one fact. */
|
|
73
|
+
cited: number;
|
|
74
|
+
/** Claims with no citation. */
|
|
75
|
+
uncited: number;
|
|
76
|
+
/** Distinct cited ids that resolve to a real atom. */
|
|
77
|
+
valid: string[];
|
|
78
|
+
/** Distinct cited ids with no matching atom -- invented provenance. */
|
|
79
|
+
unknown: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check that a model's claims cite only facts that exist in the context -- the
|
|
83
|
+
* accuracy half of "novel and accurate". A claim citing an id not in `ctx`
|
|
84
|
+
* fabricated its provenance, so `ok` is false and the id lands in `unknown`.
|
|
85
|
+
*
|
|
86
|
+
* @param claims The model's statements with their cited ids.
|
|
87
|
+
* @param ctx The {@link InterpretationContext} the brief was built from.
|
|
88
|
+
* @returns A {@link CitationAudit}.
|
|
89
|
+
*/
|
|
90
|
+
export declare function auditCitations(claims: Claim[], ctx: InterpretationContext): CitationAudit;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Default instruction header prepended to {@link Brief.prompt}. */
|
|
2
|
+
export const BRIEF_INSTRUCTIONS = "Natal chart facts follow, each with a stable id in [brackets]. Interpret them "
|
|
3
|
+
+ "in your own words; after each statement, cite the id(s) it rests on as [id]. "
|
|
4
|
+
+ "Do not introduce astrological facts that are not listed here.";
|
|
5
|
+
/** How to frame an interpretation given what the chart is. */
|
|
6
|
+
const REALM_FRAMING = {
|
|
7
|
+
observed: "", reported: "",
|
|
8
|
+
planned: "This is a planned future moment; frame statements as potentials, not settled facts.",
|
|
9
|
+
forecast: "This is a forecast moment; frame statements as tendencies, not certainties.",
|
|
10
|
+
fictional: "This is a fictional subject; interpret the symbolism, not a real person's life.",
|
|
11
|
+
mythic: "This is a mythic subject; read it as a symbol or story, not a biography.",
|
|
12
|
+
counterfactual: "This is a hypothetical variant of a real event; keep it conditional.",
|
|
13
|
+
archetypal: "This is an archetype, not a person; interpret the configuration's meaning itself.",
|
|
14
|
+
conceptual: "This is a concept or organization, not a person; interpret it as such.",
|
|
15
|
+
};
|
|
16
|
+
/** A one-line framing for the realm and certainty, or `""` when neither needs it. */
|
|
17
|
+
export function realmFraming(realm, certainty) {
|
|
18
|
+
const parts = [];
|
|
19
|
+
if (realm && REALM_FRAMING[realm])
|
|
20
|
+
parts.push(REALM_FRAMING[realm]);
|
|
21
|
+
if (certainty && certainty !== "exact") {
|
|
22
|
+
parts.push(`The time is ${certainty}, so the Moon, the angles, and the houses are uncertain`
|
|
23
|
+
+ " -- lean on the slower planets and sign-level statements.");
|
|
24
|
+
}
|
|
25
|
+
return parts.join(" ");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render an {@link InterpretationContext} as a compact, id-tagged {@link Brief}
|
|
29
|
+
* for an LLM to interpret and cite.
|
|
30
|
+
*
|
|
31
|
+
* @param ctx A projection from {@link interpretationContext}.
|
|
32
|
+
* @param opts Capping, kind filter, an optional {@link Reading} to fold in, and
|
|
33
|
+
* whether to prepend {@link BRIEF_INSTRUCTIONS}.
|
|
34
|
+
* @returns The {@link Brief}: a ranked `facts` list and a ready `prompt`.
|
|
35
|
+
*/
|
|
36
|
+
export function chartBrief(ctx, opts = {}) {
|
|
37
|
+
let atoms = ctx.atoms;
|
|
38
|
+
if (opts.kinds)
|
|
39
|
+
atoms = atoms.filter((a) => opts.kinds.includes(a.kind));
|
|
40
|
+
if (opts.minSalience !== undefined)
|
|
41
|
+
atoms = atoms.filter((a) => a.salience >= opts.minSalience);
|
|
42
|
+
if (opts.limit !== undefined)
|
|
43
|
+
atoms = atoms.slice(0, opts.limit);
|
|
44
|
+
const facts = atoms.map((a) => ({
|
|
45
|
+
id: a.id, kind: a.kind, text: a.text, salience: a.salience,
|
|
46
|
+
}));
|
|
47
|
+
const lines = facts.map((f) => `[${f.id}] ${f.text}`);
|
|
48
|
+
const framing = realmFraming(ctx.realm, ctx.certainty);
|
|
49
|
+
let prompt = (opts.header === false ? "" : `${BRIEF_INSTRUCTIONS}\n\n`)
|
|
50
|
+
+ (framing ? `${framing}\n\n` : "")
|
|
51
|
+
+ lines.join("\n");
|
|
52
|
+
if (opts.reading && opts.reading.entries.length) {
|
|
53
|
+
prompt += "\n\nSuggested readings (cite the same fact ids):\n"
|
|
54
|
+
+ opts.reading.entries
|
|
55
|
+
.map((e) => `${e.atomIds.map((id) => `[${id}]`).join("")} ${e.text}`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
}
|
|
58
|
+
return { jdUt: ctx.jdUt, zodiac: ctx.zodiac, facts, prompt };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check that a model's claims cite only facts that exist in the context -- the
|
|
62
|
+
* accuracy half of "novel and accurate". A claim citing an id not in `ctx`
|
|
63
|
+
* fabricated its provenance, so `ok` is false and the id lands in `unknown`.
|
|
64
|
+
*
|
|
65
|
+
* @param claims The model's statements with their cited ids.
|
|
66
|
+
* @param ctx The {@link InterpretationContext} the brief was built from.
|
|
67
|
+
* @returns A {@link CitationAudit}.
|
|
68
|
+
*/
|
|
69
|
+
export function auditCitations(claims, ctx) {
|
|
70
|
+
const ids = new Set(ctx.atoms.map((a) => a.id));
|
|
71
|
+
const valid = new Set();
|
|
72
|
+
const unknown = new Set();
|
|
73
|
+
let cited = 0;
|
|
74
|
+
for (const c of claims) {
|
|
75
|
+
if (c.cites.length)
|
|
76
|
+
cited++;
|
|
77
|
+
for (const id of c.cites)
|
|
78
|
+
(ids.has(id) ? valid : unknown).add(id);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
ok: unknown.size === 0,
|
|
82
|
+
claims: claims.length, cited, uncited: claims.length - cited,
|
|
83
|
+
valid: [...valid], unknown: [...unknown],
|
|
84
|
+
};
|
|
85
|
+
}
|
package/dist/src/chart.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** astroengine chart -- public API: natal charts, aspects, retrogrades. */
|
|
2
2
|
import { EngineData, AYANAMSA_J2000 } from "./core.js";
|
|
3
|
+
import type { AspectPhase } from "./electional.js";
|
|
3
4
|
export declare const BODIES: readonly ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto", "chiron", "mean_node", "true_node"];
|
|
4
5
|
export type Body = (typeof BODIES)[number];
|
|
5
6
|
/** Computable on request (not in the default chart set). */
|
|
@@ -130,6 +131,10 @@ export interface Aspect {
|
|
|
130
131
|
aspect: string;
|
|
131
132
|
/** Orb from exact, in degrees. */
|
|
132
133
|
orb: number;
|
|
134
|
+
/** Applying, separating, or exact -- from the two bodies' longitude speeds. */
|
|
135
|
+
phase: AspectPhase;
|
|
136
|
+
/** Closeness in `[0, 1]`: `1` exact, `0` at the orb limit. */
|
|
137
|
+
strength: number;
|
|
133
138
|
}
|
|
134
139
|
/** A full natal chart, as returned by {@link Engine.chart} and
|
|
135
140
|
* {@link Engine.chartAt}. Longitudes are degrees in the chart's `zodiac`. */
|
package/dist/src/chart.js
CHANGED
|
@@ -200,7 +200,10 @@ export class Engine {
|
|
|
200
200
|
: moonApparentSeries(this.data, jde);
|
|
201
201
|
return [lon, lat, km / KM_PER_AU];
|
|
202
202
|
}
|
|
203
|
-
|
|
203
|
+
// Pluto: a wide-range Chebyshev pack when one is loaded (same heliocentric
|
|
204
|
+
// pipeline as Chiron, via the generic packed-body path below), else the
|
|
205
|
+
// Meeus ch.37 series (valid 1885-2099, accuracy degrades outside).
|
|
206
|
+
if (body === "pluto" && !this.data.chebPacks?.pluto)
|
|
204
207
|
return plutoApparent(this.data, jde);
|
|
205
208
|
if (body === "chiron") {
|
|
206
209
|
if (!this.chironCheb)
|
|
@@ -346,7 +349,7 @@ export class Engine {
|
|
|
346
349
|
let l;
|
|
347
350
|
let b;
|
|
348
351
|
let r;
|
|
349
|
-
if (body === "pluto") {
|
|
352
|
+
if (body === "pluto" && !this.data.chebPacks?.pluto) {
|
|
350
353
|
[l, b, r] = plutoHeliocentric(this.data, jde);
|
|
351
354
|
[l, b] = precessEcliptic(l, b, J2000, jde);
|
|
352
355
|
}
|
|
@@ -628,11 +631,24 @@ export function findAspects(bodies, orbs = DEFAULT_ORBS) {
|
|
|
628
631
|
for (let j = i + 1; j < names.length; j++) {
|
|
629
632
|
const a = names[i];
|
|
630
633
|
const b = names[j];
|
|
631
|
-
const
|
|
634
|
+
const e = mod(bodies[a].lon - bodies[b].lon + 180, 360) - 180; // signed gap
|
|
635
|
+
const sep = Math.abs(e);
|
|
632
636
|
for (const [asp, angle] of Object.entries(ASPECTS)) {
|
|
633
637
|
const orb = Math.abs(sep - angle);
|
|
634
638
|
if (orb <= orbs[asp]) {
|
|
635
|
-
|
|
639
|
+
const orbRounded = Math.round(orb * 100) / 100;
|
|
640
|
+
// Applying/separating from the closing of the signed orb (matches
|
|
641
|
+
// electional.aspectPhase); strength from the same rounded orb so a
|
|
642
|
+
// consumer can reproduce it from .orb and the orb policy.
|
|
643
|
+
const signedOrb = sep - angle;
|
|
644
|
+
const dAbsOrbDt = (signedOrb >= 0 ? 1 : -1) * (e >= 0 ? 1 : -1)
|
|
645
|
+
* (bodies[a].speed - bodies[b].speed);
|
|
646
|
+
const phase = Math.abs(signedOrb) < 1e-9
|
|
647
|
+
? "exact" : dAbsOrbDt < 0 ? "applying" : "separating";
|
|
648
|
+
out.push({
|
|
649
|
+
a, b, aspect: asp, orb: orbRounded,
|
|
650
|
+
phase, strength: Math.max(0, 1 - orbRounded / orbs[asp]),
|
|
651
|
+
});
|
|
636
652
|
}
|
|
637
653
|
}
|
|
638
654
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine counterfactual -- a real chart, perturbed, and what changed.
|
|
3
|
+
*
|
|
4
|
+
* The `counterfactual` realm: take a resolved chart and ask "what if". Shift the
|
|
5
|
+
* instant ("born an hour later") or the place -- a real ephemeris recompute --
|
|
6
|
+
* or splice a body to a new longitude ("Mars in the next sign") -- a geometry
|
|
7
|
+
* what-if that keeps everything else and recomputes the aspects it touches.
|
|
8
|
+
* {@link chartDiff} reports the difference so the change is legible, not buried
|
|
9
|
+
* in two full charts.
|
|
10
|
+
*/
|
|
11
|
+
import { Engine, Chart, ChartOptions, Aspect } from "./chart.js";
|
|
12
|
+
import { AnchorRegistry } from "./provenance.js";
|
|
13
|
+
import { AnchoredChart, RealizedChart } from "./anchored.js";
|
|
14
|
+
/** A perturbation of a resolved chart. */
|
|
15
|
+
export interface CounterfactualEdit {
|
|
16
|
+
/** Shift the resolved instant by a duration (e.g. `"1h"`, `"-30m"`, `"P1D"`). */
|
|
17
|
+
shiftTime?: string;
|
|
18
|
+
/** Recompute at a different place. */
|
|
19
|
+
place?: {
|
|
20
|
+
lat: number;
|
|
21
|
+
lonEast: number;
|
|
22
|
+
altM?: number;
|
|
23
|
+
};
|
|
24
|
+
/** Move bodies to given ecliptic longitudes (degrees), keeping everything else
|
|
25
|
+
* -- a geometry what-if. The moved body's house and the touched aspects are
|
|
26
|
+
* recomputed; the angles and other bodies are untouched. */
|
|
27
|
+
setLongitudes?: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
/** A body whose sign or house changed between two charts. */
|
|
30
|
+
export interface BodyChange {
|
|
31
|
+
body: string;
|
|
32
|
+
/** Signed degrees the body moved (`b` minus `a`). */
|
|
33
|
+
dLon: number;
|
|
34
|
+
signFrom: string;
|
|
35
|
+
signTo: string;
|
|
36
|
+
houseFrom: number;
|
|
37
|
+
houseTo: number;
|
|
38
|
+
}
|
|
39
|
+
/** An angle whose sign changed. */
|
|
40
|
+
export interface AngleChange {
|
|
41
|
+
angle: string;
|
|
42
|
+
from: string;
|
|
43
|
+
to: string;
|
|
44
|
+
}
|
|
45
|
+
/** What differs between two charts. */
|
|
46
|
+
export interface ChartDiff {
|
|
47
|
+
/** Bodies whose sign or house changed. */
|
|
48
|
+
bodies: BodyChange[];
|
|
49
|
+
/** Aspects present in the variant but not the original. */
|
|
50
|
+
aspectsGained: Aspect[];
|
|
51
|
+
/** Aspects present in the original but not the variant. */
|
|
52
|
+
aspectsLost: Aspect[];
|
|
53
|
+
/** Angles whose sign changed. */
|
|
54
|
+
angles: AngleChange[];
|
|
55
|
+
}
|
|
56
|
+
/** Diff two charts: body sign/house shifts, aspects gained/lost, angle sign
|
|
57
|
+
* changes. Bodies and angles that did not change sign/house are omitted. */
|
|
58
|
+
export declare function chartDiff(a: Chart, b: Chart): ChartDiff;
|
|
59
|
+
/** A counterfactual: a base chart and a perturbed variant, with the diff. */
|
|
60
|
+
export interface Counterfactual {
|
|
61
|
+
edit: CounterfactualEdit;
|
|
62
|
+
/** The realized base (its `chart` is the original). */
|
|
63
|
+
original: RealizedChart;
|
|
64
|
+
/** The perturbed chart, or null when the base had no chart to perturb. */
|
|
65
|
+
variant: Chart | null;
|
|
66
|
+
diff: ChartDiff | null;
|
|
67
|
+
note: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Realize an {@link AnchoredChart}, then apply a {@link CounterfactualEdit} and
|
|
71
|
+
* diff the result -- "a real event, perturbed." A time/place edit recomputes the
|
|
72
|
+
* ephemeris; a `setLongitudes` edit splices the geometry.
|
|
73
|
+
*
|
|
74
|
+
* @param engine The engine.
|
|
75
|
+
* @param base The base chart to perturb (realized via {@link realize}).
|
|
76
|
+
* @param edit The perturbation.
|
|
77
|
+
* @param registry Anchor lookups for the base.
|
|
78
|
+
* @param opts Chart options (house system, zodiac).
|
|
79
|
+
* @returns A {@link Counterfactual}; `variant`/`diff` are null when the base
|
|
80
|
+
* produced no chart (e.g. a constraints-only form).
|
|
81
|
+
*/
|
|
82
|
+
export declare function counterfactual(engine: Engine, base: AnchoredChart, edit: CounterfactualEdit, registry?: AnchorRegistry, opts?: ChartOptions): Counterfactual;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astroengine counterfactual -- a real chart, perturbed, and what changed.
|
|
3
|
+
*
|
|
4
|
+
* The `counterfactual` realm: take a resolved chart and ask "what if". Shift the
|
|
5
|
+
* instant ("born an hour later") or the place -- a real ephemeris recompute --
|
|
6
|
+
* or splice a body to a new longitude ("Mars in the next sign") -- a geometry
|
|
7
|
+
* what-if that keeps everything else and recomputes the aspects it touches.
|
|
8
|
+
* {@link chartDiff} reports the difference so the change is legible, not buried
|
|
9
|
+
* in two full charts.
|
|
10
|
+
*/
|
|
11
|
+
import { SIGNS, findAspects, DEFAULT_ORBS, } from "./chart.js";
|
|
12
|
+
import { houseOf } from "./electional.js";
|
|
13
|
+
import { mod } from "./core.js";
|
|
14
|
+
import { parseOffset } from "./provenance.js";
|
|
15
|
+
import { realize } from "./anchored.js";
|
|
16
|
+
const signOf = (lon) => SIGNS[Math.floor(mod(lon, 360) / 30)];
|
|
17
|
+
const aspectKey = (x) => `${[x.a, x.b].sort().join("~")}:${x.aspect}`;
|
|
18
|
+
/** Diff two charts: body sign/house shifts, aspects gained/lost, angle sign
|
|
19
|
+
* changes. Bodies and angles that did not change sign/house are omitted. */
|
|
20
|
+
export function chartDiff(a, b) {
|
|
21
|
+
const bodies = [];
|
|
22
|
+
for (const [name, pa] of Object.entries(a.bodies)) {
|
|
23
|
+
const pb = b.bodies[name];
|
|
24
|
+
if (!pa || !pb || (pa.sign === pb.sign && pa.house === pb.house))
|
|
25
|
+
continue;
|
|
26
|
+
bodies.push({
|
|
27
|
+
body: name, dLon: mod(pb.lon - pa.lon + 180, 360) - 180,
|
|
28
|
+
signFrom: pa.sign, signTo: pb.sign, houseFrom: pa.house, houseTo: pb.house,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const aset = new Set(a.aspects.map(aspectKey));
|
|
32
|
+
const bset = new Set(b.aspects.map(aspectKey));
|
|
33
|
+
const angles = [];
|
|
34
|
+
for (const ang of ["asc", "mc", "vertex", "eastPoint"]) {
|
|
35
|
+
const from = signOf(a.angles[ang]);
|
|
36
|
+
const to = signOf(b.angles[ang]);
|
|
37
|
+
if (from !== to)
|
|
38
|
+
angles.push({ angle: ang, from, to });
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
bodies,
|
|
42
|
+
aspectsGained: b.aspects.filter((x) => !aset.has(aspectKey(x))),
|
|
43
|
+
aspectsLost: a.aspects.filter((x) => !bset.has(aspectKey(x))),
|
|
44
|
+
angles,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Splice bodies to new longitudes, recomputing their sign/house and the
|
|
48
|
+
* aspects (the angles and untouched bodies stay as they were). */
|
|
49
|
+
function spliceLongitudes(chart, overrides) {
|
|
50
|
+
const bodies = { ...chart.bodies };
|
|
51
|
+
for (const [name, lon] of Object.entries(overrides)) {
|
|
52
|
+
const cur = bodies[name];
|
|
53
|
+
if (!cur)
|
|
54
|
+
continue;
|
|
55
|
+
const L = mod(lon, 360);
|
|
56
|
+
bodies[name] = {
|
|
57
|
+
...cur, lon: L, sign: SIGNS[Math.floor(L / 30)], signDeg: L % 30,
|
|
58
|
+
house: houseOf(L, chart.cusps),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...chart, bodies: bodies,
|
|
63
|
+
aspects: findAspects(bodies, DEFAULT_ORBS),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Realize an {@link AnchoredChart}, then apply a {@link CounterfactualEdit} and
|
|
68
|
+
* diff the result -- "a real event, perturbed." A time/place edit recomputes the
|
|
69
|
+
* ephemeris; a `setLongitudes` edit splices the geometry.
|
|
70
|
+
*
|
|
71
|
+
* @param engine The engine.
|
|
72
|
+
* @param base The base chart to perturb (realized via {@link realize}).
|
|
73
|
+
* @param edit The perturbation.
|
|
74
|
+
* @param registry Anchor lookups for the base.
|
|
75
|
+
* @param opts Chart options (house system, zodiac).
|
|
76
|
+
* @returns A {@link Counterfactual}; `variant`/`diff` are null when the base
|
|
77
|
+
* produced no chart (e.g. a constraints-only form).
|
|
78
|
+
*/
|
|
79
|
+
export function counterfactual(engine, base, edit, registry = {}, opts = {}) {
|
|
80
|
+
const original = realize(engine, base, registry, opts);
|
|
81
|
+
if (!original.chart) {
|
|
82
|
+
return { edit, original, variant: null, diff: null, note: `nothing to perturb (${original.note})` };
|
|
83
|
+
}
|
|
84
|
+
const shiftsTimeOrPlace = edit.shiftTime !== undefined || edit.place !== undefined;
|
|
85
|
+
let variant = original.chart;
|
|
86
|
+
if (shiftsTimeOrPlace) {
|
|
87
|
+
const off = edit.shiftTime !== undefined ? parseOffset(edit.shiftTime) : 0;
|
|
88
|
+
if (Number.isNaN(off))
|
|
89
|
+
throw new Error(`unparseable shiftTime ${edit.shiftTime}`);
|
|
90
|
+
const lat = edit.place?.lat ?? original.place.place?.lat ?? 0;
|
|
91
|
+
const lon = edit.place?.lonEast ?? original.place.place?.lonEast ?? 0;
|
|
92
|
+
variant = engine.chartAt(original.time.jd + off, lat, lon, opts);
|
|
93
|
+
}
|
|
94
|
+
if (edit.setLongitudes)
|
|
95
|
+
variant = spliceLongitudes(variant, edit.setLongitudes);
|
|
96
|
+
const bits = [
|
|
97
|
+
edit.shiftTime !== undefined ? `time ${edit.shiftTime}` : null,
|
|
98
|
+
edit.place ? "place" : null,
|
|
99
|
+
edit.setLongitudes ? `moved ${Object.keys(edit.setLongitudes).join(", ")}` : null,
|
|
100
|
+
].filter(Boolean);
|
|
101
|
+
return {
|
|
102
|
+
edit, original, variant, diff: chartDiff(original.chart, variant),
|
|
103
|
+
note: bits.length ? `perturbed: ${bits.join("; ")}` : "no edit applied",
|
|
104
|
+
};
|
|
105
|
+
}
|