ether-to-astro 1.0.2 → 1.2.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.
Files changed (60) hide show
  1. package/.github/ISSUE_TEMPLATE/bug-report.yml +87 -0
  2. package/.github/ISSUE_TEMPLATE/capability-request.yml +117 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. package/.github/ISSUE_TEMPLATE/paper-cut.yml +59 -0
  5. package/.github/pull_request_template.md +1 -0
  6. package/.github/workflows/release.yml +2 -2
  7. package/.github/workflows/test.yml +2 -2
  8. package/AGENTS.md +46 -1
  9. package/DEVELOPER.md +78 -0
  10. package/README.md +128 -75
  11. package/SETUP.md +100 -41
  12. package/dist/astro-service.d.ts +51 -2
  13. package/dist/astro-service.js +660 -56
  14. package/dist/cli.js +31 -0
  15. package/dist/entrypoint.d.ts +13 -0
  16. package/dist/entrypoint.js +78 -0
  17. package/dist/ephemeris.d.ts +15 -0
  18. package/dist/ephemeris.js +33 -0
  19. package/dist/formatter.d.ts +5 -1
  20. package/dist/formatter.js +4 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.js +63 -114
  23. package/dist/loader.d.ts +1 -1
  24. package/dist/loader.js +61 -23
  25. package/dist/mcp-alias.d.ts +2 -0
  26. package/dist/mcp-alias.js +8 -0
  27. package/dist/time-utils.d.ts +8 -0
  28. package/dist/time-utils.js +16 -0
  29. package/dist/tool-registry.js +111 -5
  30. package/dist/tool-result.d.ts +8 -0
  31. package/dist/tool-result.js +39 -0
  32. package/dist/types.d.ts +79 -1
  33. package/docs/product/adrs/0001-mcp-vs-skill-boundary.md +96 -0
  34. package/docs/product/architecture-boundaries.md +223 -0
  35. package/docs/product/product-tenets.md +174 -0
  36. package/docs/releases/1.2.0-draft.md +48 -0
  37. package/package.json +7 -7
  38. package/skills/.curated/daily-brief/SKILL.md +75 -0
  39. package/skills/.curated/electional-overlay/SKILL.md +67 -0
  40. package/skills/.curated/weekly-overview/SKILL.md +73 -0
  41. package/skills/.system/write-skill/SKILL.md +90 -0
  42. package/src/astro-service.ts +861 -60
  43. package/src/cli.ts +84 -0
  44. package/src/entrypoint.ts +118 -0
  45. package/src/ephemeris.ts +44 -0
  46. package/src/formatter.ts +13 -1
  47. package/src/index.ts +77 -121
  48. package/src/loader.ts +69 -25
  49. package/src/mcp-alias.ts +10 -0
  50. package/src/time-utils.ts +18 -0
  51. package/src/tool-registry.ts +129 -9
  52. package/src/tool-result.ts +44 -0
  53. package/src/types.ts +91 -1
  54. package/tests/unit/astro-service.test.ts +751 -5
  55. package/tests/unit/cli-commands.test.ts +13 -0
  56. package/tests/unit/entrypoint.test.ts +67 -0
  57. package/tests/unit/error-mapping.test.ts +20 -0
  58. package/tests/unit/formatter.test.ts +6 -0
  59. package/tests/unit/tool-registry.test.ts +114 -2
  60. package/setup.sh +0 -21
@@ -7,9 +7,9 @@ import { EphemerisCalculator } from './ephemeris.js';
7
7
  import { formatDateOnly, formatInTimezone } from './formatter.js';
8
8
  import { HouseCalculator } from './houses.js';
9
9
  import { RiseSetCalculator } from './riseset.js';
10
- import { addLocalDays, localToUTC, utcToLocal } from './time-utils.js';
10
+ import { addLocalDays, formatLocalTimestampWithOffset, localToUTC, utcToLocal, } from './time-utils.js';
11
11
  import { deduplicateTransits, TransitCalculator } from './transits.js';
12
- import { ASTEROIDS, NODES, OUTER_PLANETS, PERSONAL_PLANETS, PLANETS, ZODIAC_SIGNS, } from './types.js';
12
+ import { ASPECTS, ASTEROIDS, NODES, OUTER_PLANETS, PERSONAL_PLANETS, PLANET_NAMES, PLANETS, ZODIAC_SIGNS, } from './types.js';
13
13
  export function parseDateOnlyInput(dateStr) {
14
14
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
15
15
  if (!match) {
@@ -32,6 +32,38 @@ export function parseDateOnlyInput(dateStr) {
32
32
  }
33
33
  return { year, month, day, hour: 12, minute: 0 };
34
34
  }
35
+ function parseTimeOnlyInput(timeStr) {
36
+ const match = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(timeStr);
37
+ if (!match) {
38
+ throw new Error(`Invalid time format: expected HH:mm[:ss], got "${timeStr}"`);
39
+ }
40
+ const hour = Number(match[1]);
41
+ const minute = Number(match[2]);
42
+ const second = match[3] === undefined ? 0 : Number(match[3]);
43
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
44
+ throw new Error(`Invalid clock time: ${timeStr}`);
45
+ }
46
+ try {
47
+ Temporal.PlainTime.from({ hour, minute, second });
48
+ }
49
+ catch {
50
+ throw new Error(`Invalid clock time: ${timeStr}`);
51
+ }
52
+ return { hour, minute, second };
53
+ }
54
+ const ELECTIONAL_CONTEXT_PLANET_IDS = [
55
+ PLANETS.SUN,
56
+ PLANETS.MOON,
57
+ PLANETS.MERCURY,
58
+ PLANETS.VENUS,
59
+ PLANETS.MARS,
60
+ PLANETS.JUPITER,
61
+ PLANETS.SATURN,
62
+ PLANETS.URANUS,
63
+ PLANETS.NEPTUNE,
64
+ PLANETS.PLUTO,
65
+ ];
66
+ const ELECTIONAL_CONTEXT_HOUSE_SYSTEMS = ['P', 'K', 'W', 'R'];
35
67
  export class AstroService {
36
68
  ephem;
37
69
  transitCalc;
@@ -39,6 +71,7 @@ export class AstroService {
39
71
  riseSetCalc;
40
72
  eclipseCalc;
41
73
  chartRenderer;
74
+ mcpStartupDefaults;
42
75
  now;
43
76
  writeFileFn;
44
77
  constructor(deps = {}) {
@@ -48,9 +81,61 @@ export class AstroService {
48
81
  this.riseSetCalc = deps.riseSetCalc ?? new RiseSetCalculator(this.ephem);
49
82
  this.eclipseCalc = deps.eclipseCalc ?? new EclipseCalculator(this.ephem);
50
83
  this.chartRenderer = deps.chartRenderer ?? new ChartRenderer(this.ephem, this.houseCalc);
84
+ this.mcpStartupDefaults = Object.freeze({ ...(deps.mcpStartupDefaults ?? {}) });
51
85
  this.now = deps.now ?? (() => new Date());
52
86
  this.writeFileFn = deps.writeFile ?? writeFile;
53
87
  }
88
+ formatTimestamp(date, timezone) {
89
+ return formatInTimezone(date, timezone, {
90
+ weekday: this.mcpStartupDefaults.weekdayLabels ?? false,
91
+ });
92
+ }
93
+ normalizeLongitude(longitude) {
94
+ return ((longitude % 360) + 360) % 360;
95
+ }
96
+ getSignAndDegree(longitude) {
97
+ const normalized = this.normalizeLongitude(longitude);
98
+ const baseSignIndex = Math.floor(normalized / 30);
99
+ const roundedDegree = Number.parseFloat((normalized % 30).toFixed(2));
100
+ const shouldCarryToNextSign = roundedDegree >= 30;
101
+ const signIndex = shouldCarryToNextSign
102
+ ? (baseSignIndex + 1) % ZODIAC_SIGNS.length
103
+ : baseSignIndex;
104
+ return {
105
+ sign: ZODIAC_SIGNS[signIndex],
106
+ degree: shouldCarryToNextSign ? 0 : roundedDegree,
107
+ };
108
+ }
109
+ getHouseNumber(longitude, houses) {
110
+ const normalized = this.normalizeLongitude(longitude);
111
+ for (let house = 1; house <= 12; house++) {
112
+ const start = this.normalizeLongitude(houses.cusps[house]);
113
+ const nextHouse = house === 12 ? 1 : house + 1;
114
+ const end = this.normalizeLongitude(houses.cusps[nextHouse]);
115
+ const span = (end - start + 360) % 360;
116
+ const offset = (normalized - start + 360) % 360;
117
+ if (span === 0 || offset === 0 || offset < span) {
118
+ return house;
119
+ }
120
+ }
121
+ return 12;
122
+ }
123
+ resolveHouseSystem(natalChart, explicitSystem) {
124
+ return (explicitSystem ||
125
+ natalChart.requestedHouseSystem ||
126
+ this.mcpStartupDefaults.preferredHouseStyle ||
127
+ natalChart.houseSystem ||
128
+ 'P');
129
+ }
130
+ resolveTimezones(explicitReportingTimezone, natalTimezone) {
131
+ return {
132
+ calculationTimezone: natalTimezone ?? 'UTC',
133
+ reportingTimezone: this.resolveReportingTimezone(explicitReportingTimezone, natalTimezone),
134
+ };
135
+ }
136
+ resolveReportingTimezone(explicitTimezone, natalTimezone) {
137
+ return explicitTimezone ?? this.mcpStartupDefaults.preferredTimezone ?? natalTimezone ?? 'UTC';
138
+ }
54
139
  async init() {
55
140
  await this.ephem.init();
56
141
  }
@@ -91,6 +176,7 @@ export class AstroService {
91
176
  planets: positions,
92
177
  julianDay: jd,
93
178
  houseSystem: houses.system,
179
+ requestedHouseSystem: requestedHouseSystem ?? undefined,
94
180
  utcDateTime: utcComponents,
95
181
  };
96
182
  const sun = positions.find((p) => p.planet === 'Sun');
@@ -170,15 +256,27 @@ export class AstroService {
170
256
  const categories = input.categories ?? ['all'];
171
257
  const includeMundane = input.include_mundane ?? false;
172
258
  const daysAhead = input.days_ahead ?? 0;
259
+ const requestedMode = input.mode;
173
260
  const maxOrb = input.max_orb ?? 8;
174
261
  const exactOnly = input.exact_only ?? false;
175
262
  const applyingOnly = input.applying_only ?? false;
176
- if (daysAhead < 0) {
177
- throw new Error('days_ahead must be >= 0');
263
+ if (!Number.isFinite(daysAhead) || daysAhead < 0) {
264
+ throw new Error('days_ahead must be a finite number >= 0');
178
265
  }
179
- if (maxOrb < 0) {
180
- throw new Error('max_orb must be >= 0');
266
+ if (!Number.isFinite(maxOrb) || maxOrb < 0) {
267
+ throw new Error('max_orb must be a finite number >= 0');
181
268
  }
269
+ if (requestedMode !== undefined &&
270
+ requestedMode !== 'snapshot' &&
271
+ requestedMode !== 'best_hit' &&
272
+ requestedMode !== 'forecast') {
273
+ throw new Error('mode must be one of: snapshot, best_hit, forecast');
274
+ }
275
+ if (!natalChart.julianDay) {
276
+ throw new Error('Natal chart is missing julianDay. Re-run set_natal_chart to fix.');
277
+ }
278
+ const mode = requestedMode ?? (daysAhead === 0 ? 'snapshot' : 'best_hit');
279
+ const modeSource = requestedMode === undefined ? 'legacy_default' : 'explicit';
182
280
  let transitingPlanetIds = [];
183
281
  if (categories.includes('all')) {
184
282
  transitingPlanetIds = Object.values(PLANETS);
@@ -193,40 +291,70 @@ export class AstroService {
193
291
  transitingPlanetIds.push(...OUTER_PLANETS.filter((p) => !transitingPlanetIds.includes(p)));
194
292
  }
195
293
  }
196
- const timezone = natalChart.location.timezone;
294
+ const { calculationTimezone, reportingTimezone } = this.resolveTimezones(undefined, natalChart.location.timezone);
197
295
  let targetDate;
198
296
  if (dateStr) {
199
297
  const parsed = parseDateOnlyInput(dateStr);
200
- targetDate = localToUTC(parsed, timezone);
298
+ targetDate = localToUTC(parsed, calculationTimezone);
201
299
  }
202
300
  else {
203
301
  const now = this.now();
204
- const localNow = utcToLocal(now, timezone);
302
+ const localNow = utcToLocal(now, calculationTimezone);
205
303
  const localNoon = { ...localNow, hour: 12, minute: 0, second: 0 };
206
- targetDate = localToUTC(localNoon, timezone);
304
+ targetDate = localToUTC(localNoon, calculationTimezone);
207
305
  }
208
306
  const allTransits = [];
209
- const startLocal = utcToLocal(targetDate, timezone);
210
- for (let day = 0; day <= daysAhead; day++) {
211
- const dayUTC = addLocalDays(startLocal, timezone, day);
307
+ const transitsByDay = new Map();
308
+ const transitContext = new WeakMap();
309
+ const startLocal = utcToLocal(targetDate, calculationTimezone);
310
+ const effectiveDaysAhead = mode === 'snapshot' ? 0 : daysAhead;
311
+ for (let day = 0; day <= effectiveDaysAhead; day++) {
312
+ const dayUTC = addLocalDays(startLocal, calculationTimezone, day);
212
313
  const jd = this.ephem.dateToJulianDay(dayUTC);
213
314
  const transitingPlanets = this.ephem.getAllPlanets(jd, transitingPlanetIds);
214
315
  const transits = this.transitCalc.findTransits(transitingPlanets, natalChart.planets || [], jd);
316
+ for (const transit of transits) {
317
+ transitContext.set(transit, { julianDay: jd });
318
+ }
215
319
  allTransits.push(...transits);
320
+ const dayLocal = utcToLocal(dayUTC, reportingTimezone);
321
+ const dayLabel = `${dayLocal.year}-${String(dayLocal.month).padStart(2, '0')}-${String(dayLocal.day).padStart(2, '0')}`;
322
+ transitsByDay.set(dayLabel, transits);
216
323
  }
217
- let filteredTransits = deduplicateTransits(allTransits);
218
- filteredTransits = filteredTransits.filter((t) => t.orb <= maxOrb);
219
- if (exactOnly)
220
- filteredTransits = filteredTransits.filter((t) => t.exactTime !== undefined);
221
- if (applyingOnly)
222
- filteredTransits = filteredTransits.filter((t) => t.isApplying);
223
- filteredTransits.sort((a, b) => a.orb - b.orb);
224
- const localDate = utcToLocal(targetDate, timezone);
225
- const dateLabel = `${localDate.year}-${String(localDate.month).padStart(2, '0')}-${String(localDate.day).padStart(2, '0')}`;
226
- const structuredData = {
227
- date: dateLabel,
228
- timezone,
229
- transits: filteredTransits.map((t) => ({
324
+ const filterTransits = (transits) => {
325
+ let filtered = transits.filter((t) => t.orb <= maxOrb);
326
+ if (exactOnly)
327
+ filtered = filtered.filter((t) => t.exactTime !== undefined);
328
+ if (applyingOnly)
329
+ filtered = filtered.filter((t) => t.isApplying);
330
+ filtered.sort((a, b) => a.orb - b.orb);
331
+ return filtered;
332
+ };
333
+ const chartHouseSystem = this.resolveHouseSystem(natalChart);
334
+ const natalHouses = this.houseCalc.calculateHouses(natalChart.julianDay, natalChart.location.latitude, natalChart.location.longitude, chartHouseSystem);
335
+ const transitHouseCache = new Map();
336
+ const planetIdsByName = new Map(Object.entries(PLANET_NAMES).map(([id, name]) => [name, Number(id)]));
337
+ const getTransitHouses = (julianDay) => {
338
+ const cached = transitHouseCache.get(julianDay);
339
+ if (cached) {
340
+ return cached;
341
+ }
342
+ const houses = this.houseCalc.calculateHouses(julianDay, natalChart.location.latitude, natalChart.location.longitude, chartHouseSystem);
343
+ transitHouseCache.set(julianDay, houses);
344
+ return houses;
345
+ };
346
+ const serializeTransit = (t) => {
347
+ const transitPlacement = this.getSignAndDegree(t.transitLongitude);
348
+ const natalPlacement = this.getSignAndDegree(t.natalLongitude);
349
+ const context = transitContext.get(t);
350
+ const transitHouseJulianDay = t.exactTime
351
+ ? this.ephem.dateToJulianDay(t.exactTime)
352
+ : (context?.julianDay ?? this.ephem.dateToJulianDay(targetDate));
353
+ const transitHouses = getTransitHouses(transitHouseJulianDay);
354
+ const exactTransitLongitude = t.exactTime && planetIdsByName.has(t.transitingPlanet)
355
+ ? this.ephem.getPlanetPosition(planetIdsByName.get(t.transitingPlanet), transitHouseJulianDay).longitude
356
+ : t.transitLongitude;
357
+ return {
230
358
  transitingPlanet: t.transitingPlanet,
231
359
  aspect: t.aspect,
232
360
  natalPlanet: t.natalPlanet,
@@ -236,41 +364,323 @@ export class AstroService {
236
364
  exactTime: t.exactTime?.toISOString(),
237
365
  transitLongitude: t.transitLongitude,
238
366
  natalLongitude: t.natalLongitude,
239
- })),
367
+ transitSign: transitPlacement.sign,
368
+ transitDegree: transitPlacement.degree,
369
+ transitHouse: this.getHouseNumber(exactTransitLongitude, transitHouses),
370
+ natalSign: natalPlacement.sign,
371
+ natalDegree: natalPlacement.degree,
372
+ natalHouse: this.getHouseNumber(t.natalLongitude, natalHouses),
373
+ };
374
+ };
375
+ const filteredTransits = mode === 'forecast'
376
+ ? filterTransits(deduplicateTransits(allTransits))
377
+ : filterTransits(deduplicateTransits(allTransits));
378
+ const localDate = utcToLocal(targetDate, reportingTimezone);
379
+ const dateLabel = `${localDate.year}-${String(localDate.month).padStart(2, '0')}-${String(localDate.day).padStart(2, '0')}`;
380
+ const endLocal = utcToLocal(addLocalDays(startLocal, calculationTimezone, effectiveDaysAhead), reportingTimezone);
381
+ const windowEndLabel = `${endLocal.year}-${String(endLocal.month).padStart(2, '0')}-${String(endLocal.day).padStart(2, '0')}`;
382
+ const structuredData = {
383
+ date: dateLabel,
384
+ timezone: reportingTimezone,
385
+ calculation_timezone: calculationTimezone,
386
+ reporting_timezone: reportingTimezone,
387
+ transits: filteredTransits.map(serializeTransit),
388
+ };
389
+ const metadata = {
390
+ mode,
391
+ mode_source: modeSource,
392
+ days_ahead: effectiveDaysAhead,
393
+ window_start: dateLabel,
394
+ window_end: windowEndLabel,
240
395
  };
241
396
  let responseData = structuredData;
242
397
  let mundaneText = '';
398
+ if (mode === 'forecast') {
399
+ const forecastDays = Array.from(transitsByDay.entries())
400
+ .sort(([a], [b]) => a.localeCompare(b))
401
+ .map(([dayDate, dayTransits]) => ({
402
+ date: dayDate,
403
+ transits: filterTransits(deduplicateTransits(dayTransits)).map(serializeTransit),
404
+ }));
405
+ responseData = {
406
+ ...metadata,
407
+ timezone: reportingTimezone,
408
+ calculation_timezone: calculationTimezone,
409
+ reporting_timezone: reportingTimezone,
410
+ forecast: forecastDays,
411
+ };
412
+ }
413
+ else {
414
+ responseData = {
415
+ ...structuredData,
416
+ ...metadata,
417
+ };
418
+ }
243
419
  if (includeMundane) {
244
- const currentJD = this.ephem.dateToJulianDay(targetDate);
245
- const currentPositions = this.ephem.getAllPlanets(currentJD, transitingPlanetIds);
420
+ const mundaneDays = [];
421
+ for (let day = 0; day <= daysAhead; day++) {
422
+ const dayUTC = addLocalDays(startLocal, calculationTimezone, day);
423
+ mundaneDays.push(this.getMundaneDay(dayUTC, reportingTimezone, transitingPlanetIds));
424
+ }
425
+ const [anchorMundane] = mundaneDays;
246
426
  const mundaneData = {
247
- date: dateLabel,
248
- timezone,
249
- positions: currentPositions,
427
+ date: anchorMundane.date,
428
+ timezone: anchorMundane.timezone,
429
+ positions: anchorMundane.positions,
430
+ aspects: anchorMundane.aspects,
431
+ days: mundaneDays,
250
432
  };
251
- responseData = { transits: structuredData, mundane: mundaneData };
252
- mundaneText = `\n\nCurrent Planetary Positions:\n\n${currentPositions
433
+ responseData = { transits: responseData, mundane: mundaneData };
434
+ mundaneText = `\n\nCurrent Planetary Positions:\n\n${anchorMundane.positions
253
435
  .map((p) => `${p.planet}: ${p.degree.toFixed(1)}° ${p.sign} (${p.isRetrograde ? 'Rx' : 'Direct'})`)
254
436
  .join('\n')}`;
437
+ if (mode === 'forecast') {
438
+ mundaneText +=
439
+ '\n\nNote: mundane positions remain anchored to the forecast start date in this mode.';
440
+ }
255
441
  }
256
- const humanLines = filteredTransits
257
- .map((t) => {
258
- const exactStr = t.exactTime ? ` - Exact: ${formatInTimezone(t.exactTime, timezone)}` : '';
442
+ const formatHumanTransit = (t) => {
443
+ const exactStr = t.exactTime
444
+ ? ` - Exact: ${this.formatTimestamp(t.exactTime, reportingTimezone)}`
445
+ : '';
259
446
  const applyStr = t.isApplying ? '(applying)' : '(separating)';
260
447
  return `${t.transitingPlanet} ${t.aspect} ${t.natalPlanet}: ${t.orb.toFixed(2)}° orb ${applyStr}${exactStr}`;
261
- })
262
- .join('\n');
263
- const rangeStr = daysAhead > 0 ? ` (next ${daysAhead + 1} days)` : '';
264
- const transitHeader = filteredTransits.length > 0
265
- ? `Transits${rangeStr}:\n\n${humanLines}`
266
- : 'No transits found matching the specified criteria.';
448
+ };
449
+ const rangeStr = effectiveDaysAhead > 0 ? ` (next ${effectiveDaysAhead + 1} days)` : '';
450
+ let transitHeader;
451
+ if (mode === 'forecast') {
452
+ const forecastLines = Array.from(transitsByDay.entries())
453
+ .sort(([a], [b]) => a.localeCompare(b))
454
+ .map(([dayDate, dayTransits]) => {
455
+ const dedupedDay = filterTransits(deduplicateTransits(dayTransits));
456
+ const lines = dedupedDay.length === 0
457
+ ? 'No transits found matching the specified criteria.'
458
+ : dedupedDay.map(formatHumanTransit).join('\n');
459
+ return `${dayDate}:\n${lines}`;
460
+ })
461
+ .join('\n\n');
462
+ transitHeader = `Forecast transits${rangeStr}:\n\n${forecastLines}`;
463
+ }
464
+ else {
465
+ const humanLines = filteredTransits.map(formatHumanTransit).join('\n');
466
+ const modeLabel = mode === 'snapshot' ? 'Transit snapshot' : 'Best-hit transits';
467
+ transitHeader =
468
+ filteredTransits.length > 0
469
+ ? `${modeLabel}${rangeStr}:\n\n${humanLines}`
470
+ : 'No transits found matching the specified criteria.';
471
+ }
267
472
  return {
268
473
  data: responseData,
269
474
  text: transitHeader + mundaneText,
270
475
  };
271
476
  }
477
+ getMundaneWeather(aspects) {
478
+ const supportiveAspects = new Set(['conjunction', 'trine', 'sextile']);
479
+ const challengingAspects = new Set(['square', 'opposition']);
480
+ return {
481
+ supportive: aspects.filter((a) => supportiveAspects.has(a.aspect)).map((a) => a.id),
482
+ challenging: aspects.filter((a) => challengingAspects.has(a.aspect)).map((a) => a.id),
483
+ };
484
+ }
485
+ getMundaneAspects(date, positions) {
486
+ const aspects = [];
487
+ for (let i = 0; i < positions.length; i++) {
488
+ for (let j = i + 1; j < positions.length; j++) {
489
+ const planetA = positions[i];
490
+ const planetB = positions[j];
491
+ const angle = this.ephem.calculateAspectAngle(planetA.longitude, planetB.longitude);
492
+ for (const aspect of ASPECTS) {
493
+ const orb = Math.abs(angle - aspect.angle);
494
+ if (orb > aspect.orb) {
495
+ continue;
496
+ }
497
+ const futureLongitudeA = (planetA.longitude + planetA.speed * 0.1 + 360) % 360;
498
+ const futureLongitudeB = (planetB.longitude + planetB.speed * 0.1 + 360) % 360;
499
+ const futureAngle = this.ephem.calculateAspectAngle(futureLongitudeA, futureLongitudeB);
500
+ const futureOrb = Math.abs(futureAngle - aspect.angle);
501
+ aspects.push({
502
+ id: `${date}:${planetA.planet}-${aspect.name}-${planetB.planet}`,
503
+ planetA: planetA.planet,
504
+ planetB: planetB.planet,
505
+ aspect: aspect.name,
506
+ orb: Number.parseFloat(orb.toFixed(2)),
507
+ isApplying: futureOrb < orb,
508
+ longitudeA: planetA.longitude,
509
+ longitudeB: planetB.longitude,
510
+ });
511
+ }
512
+ }
513
+ }
514
+ return aspects.sort((a, b) => a.orb - b.orb ||
515
+ a.planetA.localeCompare(b.planetA) ||
516
+ a.planetB.localeCompare(b.planetB) ||
517
+ a.aspect.localeCompare(b.aspect));
518
+ }
519
+ getMundaneDay(dayUTC, timezone, transitingPlanetIds) {
520
+ const localDay = utcToLocal(dayUTC, timezone);
521
+ const dateLabel = `${localDay.year}-${String(localDay.month).padStart(2, '0')}-${String(localDay.day).padStart(2, '0')}`;
522
+ const currentJD = this.ephem.dateToJulianDay(dayUTC);
523
+ const positions = this.ephem.getAllPlanets(currentJD, transitingPlanetIds);
524
+ const aspects = this.getMundaneAspects(dateLabel, positions);
525
+ return {
526
+ date: dateLabel,
527
+ timezone,
528
+ positions,
529
+ aspects,
530
+ weather: this.getMundaneWeather(aspects),
531
+ };
532
+ }
533
+ getElectionalContext(input) {
534
+ if (input.latitude < -90 || input.latitude > 90) {
535
+ throw new Error(`Invalid latitude: ${input.latitude} (must be between -90 and 90)`);
536
+ }
537
+ if (input.longitude < -180 || input.longitude > 180) {
538
+ throw new Error(`Invalid longitude: ${input.longitude} (must be between -180 and 180)`);
539
+ }
540
+ const houseSystem = input.house_system ?? 'P';
541
+ if (!ELECTIONAL_CONTEXT_HOUSE_SYSTEMS.includes(houseSystem)) {
542
+ throw new Error(`Invalid house_system: ${houseSystem} (must be one of ${ELECTIONAL_CONTEXT_HOUSE_SYSTEMS.join(', ')})`);
543
+ }
544
+ const includeRulerBasics = input.include_ruler_basics ?? false;
545
+ const includePlanetaryApplications = input.include_planetary_applications ?? true;
546
+ const orbDegrees = input.orb_degrees ?? 3;
547
+ if (!Number.isFinite(orbDegrees) || orbDegrees < 0.1 || orbDegrees > 10) {
548
+ throw new Error(`Invalid orb_degrees: ${orbDegrees} (must be between 0.1 and 10)`);
549
+ }
550
+ const parsedDate = parseDateOnlyInput(input.date);
551
+ const parsedTime = parseTimeOnlyInput(input.time);
552
+ let instantUtc;
553
+ try {
554
+ instantUtc = localToUTC({
555
+ year: parsedDate.year,
556
+ month: parsedDate.month,
557
+ day: parsedDate.day,
558
+ hour: parsedTime.hour,
559
+ minute: parsedTime.minute,
560
+ second: parsedTime.second,
561
+ }, input.timezone, 'reject');
562
+ }
563
+ catch (error) {
564
+ if (error instanceof RangeError) {
565
+ throw new Error(`Invalid local electional time: ${input.date} ${input.time} in ${input.timezone} is ambiguous or nonexistent due to a DST transition.`);
566
+ }
567
+ throw error;
568
+ }
569
+ const jdUt = this.ephem.dateToJulianDay(instantUtc);
570
+ const houses = this.houseCalc.calculateHouses(jdUt, input.latitude, input.longitude, houseSystem);
571
+ const positions = this.ephem.getAllPlanets(jdUt, ELECTIONAL_CONTEXT_PLANET_IDS);
572
+ const sun = positions.find((position) => position.planet === 'Sun');
573
+ const moon = positions.find((position) => position.planet === 'Moon');
574
+ if (!sun || !moon) {
575
+ throw new Error('Ephemeris failed to compute Sun/Moon positions for electional context.');
576
+ }
577
+ const sunHorizontal = this.ephem.getHorizontalCoordinates(jdUt, sun, input.longitude, input.latitude);
578
+ const sunAltitudeDegrees = Number.parseFloat(sunHorizontal.trueAltitude.toFixed(2));
579
+ const isDayChart = sunAltitudeDegrees >= 0;
580
+ const applyingAspects = includePlanetaryApplications
581
+ ? this.getElectionalApplyingAspects(positions, orbDegrees)
582
+ : undefined;
583
+ const moonApplyingAspects = applyingAspects?.filter((aspect) => aspect.from_body === 'Moon' || aspect.to_body === 'Moon');
584
+ const phaseAngle = Number.parseFloat(((((moon.longitude - sun.longitude) % 360) + 360) % 360).toFixed(2));
585
+ const warnings = [];
586
+ if (Math.abs(sunAltitudeDegrees) < 0.5) {
587
+ warnings.push('Sun is near the horizon; day/night classification is close to the boundary.');
588
+ }
589
+ warnings.push('Moon void-of-course is deferred in this slice and returns null.');
590
+ if (houses.system !== houseSystem) {
591
+ warnings.push(`House calculation fell back from ${houseSystem} to ${houses.system} for this location.`);
592
+ }
593
+ const ascLongitude = ((houses.ascendant % 360) + 360) % 360;
594
+ const ascSign = ZODIAC_SIGNS[Math.floor(ascLongitude / 30)];
595
+ const response = {
596
+ input: {
597
+ date: input.date,
598
+ time: input.time,
599
+ timezone: input.timezone,
600
+ latitude: input.latitude,
601
+ longitude: input.longitude,
602
+ house_system: houses.system,
603
+ instant_utc: instantUtc.toISOString(),
604
+ jd_ut: Number.parseFloat(jdUt.toFixed(8)),
605
+ },
606
+ ascendant: {
607
+ longitude: Number.parseFloat(ascLongitude.toFixed(4)),
608
+ sign: ascSign,
609
+ degree_in_sign: Number.parseFloat((ascLongitude % 30).toFixed(4)),
610
+ },
611
+ sect: {
612
+ is_day_chart: isDayChart,
613
+ sun_altitude_degrees: sunAltitudeDegrees,
614
+ classification: isDayChart ? 'day' : 'night',
615
+ },
616
+ moon: {
617
+ longitude: Number.parseFloat(moon.longitude.toFixed(4)),
618
+ sign: moon.sign,
619
+ phase_angle: phaseAngle,
620
+ phase_name: this.getElectionalPhaseName(phaseAngle),
621
+ is_void_of_course: null,
622
+ ...(moonApplyingAspects !== undefined ? { applying_aspects: moonApplyingAspects } : {}),
623
+ },
624
+ meta: {
625
+ deterministic: true,
626
+ requires_natal: false,
627
+ warnings,
628
+ deferred_features: [
629
+ 'robust_void_of_course',
630
+ 'detailed_ruler_condition',
631
+ 'house_context',
632
+ 'natal_overlays',
633
+ ],
634
+ },
635
+ };
636
+ if (applyingAspects) {
637
+ response.applying_aspects = applyingAspects;
638
+ }
639
+ if (includeRulerBasics) {
640
+ const rulerBody = this.getTraditionalSignRuler(ascSign);
641
+ const rulerPosition = positions.find((position) => position.planet === rulerBody);
642
+ if (!rulerPosition) {
643
+ throw new Error(`Ephemeris failed to compute ASC ruler position for ${rulerBody}.`);
644
+ }
645
+ response.ruler_basics = {
646
+ asc_sign_ruler: {
647
+ body: rulerBody,
648
+ longitude: Number.parseFloat(rulerPosition.longitude.toFixed(4)),
649
+ sign: rulerPosition.sign,
650
+ speed: Number.parseFloat(rulerPosition.speed.toFixed(6)),
651
+ is_retrograde: rulerPosition.isRetrograde,
652
+ },
653
+ };
654
+ }
655
+ const humanText = [
656
+ `Electional context for ${input.date} ${input.time} (${input.timezone})`,
657
+ '',
658
+ `Ascendant: ${response.ascendant.degree_in_sign.toFixed(2)}° ${response.ascendant.sign}`,
659
+ `Sect: ${response.sect.classification} (${response.sect.sun_altitude_degrees.toFixed(2)}° Sun altitude)`,
660
+ `Moon: ${response.moon.phase_name} in ${response.moon.sign} (${response.moon.phase_angle.toFixed(2)}° phase angle)`,
661
+ ];
662
+ if (includePlanetaryApplications) {
663
+ const topLevelAspectText = applyingAspects && applyingAspects.length > 0
664
+ ? applyingAspects
665
+ .slice(0, 5)
666
+ .map((aspect) => `${aspect.from_body} ${aspect.aspect} ${aspect.to_body} (${aspect.orb.toFixed(2)}°)`)
667
+ .join('\n')
668
+ : 'No applying aspects found within the configured orb.';
669
+ humanText.push('', 'Applying Aspects:', topLevelAspectText);
670
+ }
671
+ if (response.ruler_basics) {
672
+ humanText.push('', `ASC Ruler: ${response.ruler_basics.asc_sign_ruler.body} in ${response.ruler_basics.asc_sign_ruler.sign} (${response.ruler_basics.asc_sign_ruler.longitude.toFixed(2)}°)`);
673
+ }
674
+ if (warnings.length > 0) {
675
+ humanText.push('', `Warnings: ${warnings.join(' ')}`);
676
+ }
677
+ return {
678
+ data: response,
679
+ text: humanText.join('\n'),
680
+ };
681
+ }
272
682
  getHouses(natalChart, input = {}) {
273
- const system = input.system || natalChart.houseSystem || 'P';
683
+ const system = this.resolveHouseSystem(natalChart, input.system);
274
684
  if (!natalChart.julianDay) {
275
685
  throw new Error('Natal chart is missing julianDay. Re-run set_natal_chart to fix.');
276
686
  }
@@ -288,17 +698,203 @@ export class AstroService {
288
698
  text: humanText,
289
699
  };
290
700
  }
291
- getRetrogradePlanets(timezone = 'UTC') {
701
+ getRisingSignWindows(input) {
702
+ const mode = input.mode ?? 'approximate';
703
+ if (mode !== 'approximate' && mode !== 'exact') {
704
+ throw new Error(`Invalid mode: ${mode} (must be approximate or exact)`);
705
+ }
706
+ if (input.latitude < -90 || input.latitude > 90) {
707
+ throw new Error(`Invalid latitude: ${input.latitude} (must be between -90 and 90)`);
708
+ }
709
+ if (input.longitude < -180 || input.longitude > 180) {
710
+ throw new Error(`Invalid longitude: ${input.longitude} (must be between -180 and 180)`);
711
+ }
712
+ const parsed = parseDateOnlyInput(input.date);
713
+ try {
714
+ utcToLocal(new Date(), input.timezone);
715
+ }
716
+ catch {
717
+ throw new Error(`Invalid timezone: ${input.timezone}`);
718
+ }
719
+ const dayStartLocal = {
720
+ year: parsed.year,
721
+ month: parsed.month,
722
+ day: parsed.day,
723
+ hour: 0,
724
+ minute: 0,
725
+ second: 0,
726
+ };
727
+ const dayStartUtc = localToUTC(dayStartLocal, input.timezone);
728
+ const dayEndUtc = addLocalDays(dayStartLocal, input.timezone, 1);
729
+ const getAscSign = (date) => {
730
+ const jd = this.ephem.dateToJulianDay(date);
731
+ const houses = this.houseCalc.calculateHouses(jd, input.latitude, input.longitude, 'P');
732
+ const normalized = ((houses.ascendant % 360) + 360) % 360;
733
+ return { sign: ZODIAC_SIGNS[Math.floor(normalized / 30)], longitude: normalized };
734
+ };
735
+ const refineBoundary = (left, right) => {
736
+ const leftSign = getAscSign(left).sign;
737
+ let lo = left;
738
+ let hi = right;
739
+ for (let i = 0; i < 25; i++) {
740
+ const mid = new Date((lo.getTime() + hi.getTime()) / 2);
741
+ const midSign = getAscSign(mid).sign;
742
+ if (midSign === leftSign) {
743
+ lo = mid;
744
+ }
745
+ else {
746
+ hi = mid;
747
+ }
748
+ }
749
+ return hi;
750
+ };
751
+ const findSignTransitionsInBucket = (start, end, probeStepMs) => {
752
+ const boundaries = [];
753
+ let probeCursor = start;
754
+ let currentSign = getAscSign(probeCursor).sign;
755
+ while (probeCursor < end) {
756
+ const probeNext = new Date(Math.min(probeCursor.getTime() + probeStepMs, end.getTime()));
757
+ const nextSign = getAscSign(probeNext).sign;
758
+ if (nextSign !== currentSign) {
759
+ boundaries.push(mode === 'exact' ? refineBoundary(probeCursor, probeNext) : probeNext);
760
+ }
761
+ probeCursor = probeNext;
762
+ currentSign = nextSign;
763
+ }
764
+ return boundaries;
765
+ };
766
+ const stepMs = mode === 'exact' ? 60 * 60 * 1000 : 2 * 60 * 60 * 1000;
767
+ const probeStepMs = mode === 'exact' ? 5 * 60 * 1000 : 30 * 60 * 1000;
768
+ const boundaries = [dayStartUtc];
769
+ let cursor = dayStartUtc;
770
+ while (cursor < dayEndUtc) {
771
+ const next = new Date(Math.min(cursor.getTime() + stepMs, dayEndUtc.getTime()));
772
+ boundaries.push(...findSignTransitionsInBucket(cursor, next, probeStepMs));
773
+ cursor = next;
774
+ }
775
+ boundaries.push(dayEndUtc);
776
+ const windows = boundaries.slice(0, -1).map((start, i) => {
777
+ const end = boundaries[i + 1];
778
+ const sample = new Date((start.getTime() + end.getTime()) / 2);
779
+ const sign = getAscSign(sample).sign;
780
+ return {
781
+ sign,
782
+ start: formatLocalTimestampWithOffset(start, input.timezone),
783
+ end: formatLocalTimestampWithOffset(end, input.timezone),
784
+ durationMinutes: Math.round((end.getTime() - start.getTime()) / 60000),
785
+ };
786
+ });
787
+ const structuredData = {
788
+ date: input.date,
789
+ timezone: input.timezone,
790
+ location: {
791
+ latitude: input.latitude,
792
+ longitude: input.longitude,
793
+ },
794
+ mode,
795
+ windows,
796
+ };
797
+ const humanText = `Rising Sign Windows (${input.date}, ${input.timezone}, ${mode}):\n\n${windows
798
+ .map((window) => `${window.sign}: ${formatInTimezone(new Date(window.start), input.timezone)} → ${formatInTimezone(new Date(window.end), input.timezone)}`)
799
+ .join('\n')}`;
800
+ return {
801
+ data: structuredData,
802
+ text: humanText,
803
+ };
804
+ }
805
+ getElectionalApplyingAspects(positions, orbDegrees) {
806
+ const aspects = [];
807
+ for (let i = 0; i < positions.length; i++) {
808
+ for (let j = i + 1; j < positions.length; j++) {
809
+ const from = positions[i];
810
+ const to = positions[j];
811
+ const currentAngle = this.ephem.calculateAspectAngle(from.longitude, to.longitude);
812
+ for (const aspect of ASPECTS) {
813
+ const orb = Math.abs(currentAngle - aspect.angle);
814
+ if (orb > aspect.orb || orb > orbDegrees) {
815
+ continue;
816
+ }
817
+ const applying = this.isElectionalAspectApplying(from, to, aspect.angle);
818
+ if (!applying) {
819
+ continue;
820
+ }
821
+ aspects.push({
822
+ from_body: from.planet,
823
+ to_body: to.planet,
824
+ aspect: aspect.name,
825
+ orb: Number.parseFloat(orb.toFixed(4)),
826
+ applying: true,
827
+ });
828
+ }
829
+ }
830
+ }
831
+ return aspects.sort((a, b) => a.orb - b.orb ||
832
+ a.from_body.localeCompare(b.from_body) ||
833
+ a.to_body.localeCompare(b.to_body) ||
834
+ a.aspect.localeCompare(b.aspect));
835
+ }
836
+ isElectionalAspectApplying(from, to, aspectAngle) {
837
+ const signedSeparation = this.getSignedAngularDifference(from.longitude, to.longitude);
838
+ const currentSeparation = Math.abs(signedSeparation);
839
+ if (currentSeparation === aspectAngle) {
840
+ return false;
841
+ }
842
+ const separationRate = Math.sign(signedSeparation || 1) * (to.speed - from.speed);
843
+ if (separationRate === 0) {
844
+ return false;
845
+ }
846
+ return currentSeparation < aspectAngle ? separationRate > 0 : separationRate < 0;
847
+ }
848
+ getSignedAngularDifference(fromLongitude, toLongitude) {
849
+ const normalized = ((toLongitude - fromLongitude + 540) % 360) - 180;
850
+ return normalized === -180 ? 180 : normalized;
851
+ }
852
+ getElectionalPhaseName(phaseAngle) {
853
+ if (phaseAngle < 45)
854
+ return 'new';
855
+ if (phaseAngle < 90)
856
+ return 'crescent';
857
+ if (phaseAngle < 135)
858
+ return 'first_quarter';
859
+ if (phaseAngle < 180)
860
+ return 'gibbous';
861
+ if (phaseAngle < 225)
862
+ return 'full';
863
+ if (phaseAngle < 270)
864
+ return 'disseminating';
865
+ if (phaseAngle < 315)
866
+ return 'last_quarter';
867
+ return 'balsamic';
868
+ }
869
+ getTraditionalSignRuler(sign) {
870
+ const signRulers = {
871
+ Aries: 'Mars',
872
+ Taurus: 'Venus',
873
+ Gemini: 'Mercury',
874
+ Cancer: 'Moon',
875
+ Leo: 'Sun',
876
+ Virgo: 'Mercury',
877
+ Libra: 'Venus',
878
+ Scorpio: 'Mars',
879
+ Sagittarius: 'Jupiter',
880
+ Capricorn: 'Saturn',
881
+ Aquarius: 'Saturn',
882
+ Pisces: 'Jupiter',
883
+ };
884
+ return signRulers[sign] ?? 'Mars';
885
+ }
886
+ getRetrogradePlanets(timezone) {
887
+ const resolvedTimezone = this.resolveReportingTimezone(timezone);
292
888
  const now = this.now();
293
889
  const jd = this.ephem.dateToJulianDay(now);
294
890
  const allPlanetIds = Object.values(PLANETS);
295
891
  const positions = this.ephem.getAllPlanets(jd, allPlanetIds);
296
892
  const retrograde = positions.filter((p) => p.isRetrograde);
297
- const localNow = utcToLocal(now, timezone);
893
+ const localNow = utcToLocal(now, resolvedTimezone);
298
894
  const dateLabel = `${localNow.year}-${String(localNow.month).padStart(2, '0')}-${String(localNow.day).padStart(2, '0')}`;
299
895
  const structuredData = {
300
896
  date: dateLabel,
301
- timezone,
897
+ timezone: resolvedTimezone,
302
898
  planets: retrograde,
303
899
  };
304
900
  const humanText = retrograde.length === 0
@@ -308,6 +904,7 @@ export class AstroService {
308
904
  }
309
905
  async getRiseSetTimes(natalChart) {
310
906
  const timezone = natalChart.location.timezone;
907
+ const reportingTimezone = this.mcpStartupDefaults.preferredTimezone || timezone;
311
908
  const now = this.now();
312
909
  const localNow = utcToLocal(now, timezone);
313
910
  const localMidnight = {
@@ -332,8 +929,8 @@ export class AstroService {
332
929
  };
333
930
  const humanText = `Rise/Set Times:\n\n${results
334
931
  .map((r) => {
335
- const rise = r.rise ? formatInTimezone(r.rise, timezone) : 'none';
336
- const set = r.set ? formatInTimezone(r.set, timezone) : 'none';
932
+ const rise = r.rise ? this.formatTimestamp(r.rise, reportingTimezone) : 'none';
933
+ const set = r.set ? this.formatTimestamp(r.set, reportingTimezone) : 'none';
337
934
  return `${r.planet}: Rise ${rise}, Set ${set}`;
338
935
  })
339
936
  .join('\n')}`;
@@ -342,16 +939,17 @@ export class AstroService {
342
939
  text: humanText,
343
940
  };
344
941
  }
345
- getAsteroidPositions(timezone = 'UTC') {
942
+ getAsteroidPositions(timezone) {
943
+ const resolvedTimezone = this.resolveReportingTimezone(timezone);
346
944
  const now = this.now();
347
945
  const jd = this.ephem.dateToJulianDay(now);
348
946
  const asteroidIds = [...ASTEROIDS, ...NODES];
349
947
  const positions = this.ephem.getAllPlanets(jd, asteroidIds);
350
- const localNow = utcToLocal(now, timezone);
948
+ const localNow = utcToLocal(now, resolvedTimezone);
351
949
  const dateLabel = `${localNow.year}-${String(localNow.month).padStart(2, '0')}-${String(localNow.day).padStart(2, '0')}`;
352
950
  const structuredData = {
353
951
  date: dateLabel,
354
- timezone,
952
+ timezone: resolvedTimezone,
355
953
  positions,
356
954
  };
357
955
  const humanText = `Asteroid & Node Positions:\n\n${positions
@@ -365,7 +963,8 @@ export class AstroService {
365
963
  text: humanText,
366
964
  };
367
965
  }
368
- getNextEclipses(timezone = 'UTC') {
966
+ getNextEclipses(timezone) {
967
+ const resolvedTimezone = this.resolveReportingTimezone(timezone);
369
968
  const now = this.now();
370
969
  const jd = this.ephem.dateToJulianDay(now);
371
970
  const solarEclipse = this.eclipseCalc.findNextSolarEclipse(jd);
@@ -378,7 +977,7 @@ export class AstroService {
378
977
  eclipseType: solarEclipse.eclipseType,
379
978
  maxTime: solarEclipse.maxTime.toISOString(),
380
979
  });
381
- humanLines.push(`Next Solar Eclipse: ${formatInTimezone(solarEclipse.maxTime, timezone)} (${solarEclipse.eclipseType})`);
980
+ humanLines.push(`Next Solar Eclipse: ${this.formatTimestamp(solarEclipse.maxTime, resolvedTimezone)} (${solarEclipse.eclipseType})`);
382
981
  }
383
982
  if (lunarEclipse) {
384
983
  eclipses.push({
@@ -386,9 +985,9 @@ export class AstroService {
386
985
  eclipseType: lunarEclipse.eclipseType,
387
986
  maxTime: lunarEclipse.maxTime.toISOString(),
388
987
  });
389
- humanLines.push(`Next Lunar Eclipse: ${formatInTimezone(lunarEclipse.maxTime, timezone)} (${lunarEclipse.eclipseType})`);
988
+ humanLines.push(`Next Lunar Eclipse: ${this.formatTimestamp(lunarEclipse.maxTime, resolvedTimezone)} (${lunarEclipse.eclipseType})`);
390
989
  }
391
- const structuredData = { timezone, eclipses };
990
+ const structuredData = { timezone: resolvedTimezone, eclipses };
392
991
  const humanText = eclipses.length === 0
393
992
  ? 'No eclipses found in the near future.'
394
993
  : `Upcoming Eclipses:\n\n${humanLines.join('\n')}`;
@@ -400,6 +999,11 @@ export class AstroService {
400
999
  hasNatalChart: natalChart !== null,
401
1000
  natalChartName: natalChart?.name ?? null,
402
1001
  natalChartTimezone: natalChart?.location.timezone ?? null,
1002
+ startupDefaults: {
1003
+ preferredTimezone: this.mcpStartupDefaults.preferredTimezone ?? null,
1004
+ preferredHouseStyle: this.mcpStartupDefaults.preferredHouseStyle ?? null,
1005
+ weekdayLabels: this.mcpStartupDefaults.weekdayLabels ?? null,
1006
+ },
403
1007
  ephemerisInitialized: this.isInitialized(),
404
1008
  stateModel: 'stateful-per-process',
405
1009
  };