ether-to-astro 1.2.0 → 1.3.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 (66) hide show
  1. package/README.md +15 -5
  2. package/dist/astro-service/chart-output-service.d.ts +44 -0
  3. package/dist/astro-service/chart-output-service.js +110 -0
  4. package/dist/astro-service/date-input.d.ts +14 -0
  5. package/dist/astro-service/date-input.js +30 -0
  6. package/dist/astro-service/electional-service.d.ts +45 -0
  7. package/dist/astro-service/electional-service.js +305 -0
  8. package/dist/astro-service/natal-service.d.ts +41 -0
  9. package/dist/astro-service/natal-service.js +179 -0
  10. package/dist/astro-service/rising-sign-service.d.ts +37 -0
  11. package/dist/astro-service/rising-sign-service.js +137 -0
  12. package/dist/astro-service/service-types.d.ts +82 -0
  13. package/dist/astro-service/service-types.js +1 -0
  14. package/dist/astro-service/shared.d.ts +65 -0
  15. package/dist/astro-service/shared.js +98 -0
  16. package/dist/astro-service/sky-service.d.ts +48 -0
  17. package/dist/astro-service/sky-service.js +144 -0
  18. package/dist/astro-service/transit-service.d.ts +82 -0
  19. package/dist/astro-service/transit-service.js +353 -0
  20. package/dist/astro-service.d.ts +101 -89
  21. package/dist/astro-service.js +162 -1042
  22. package/dist/tool-registry.js +1 -1
  23. package/docs/product/architecture-boundaries.md +8 -0
  24. package/docs/releases/1.3.0.md +51 -0
  25. package/docs/releases/README.md +17 -0
  26. package/package.json +4 -1
  27. package/src/astro-service/chart-output-service.ts +155 -0
  28. package/src/astro-service/date-input.ts +40 -0
  29. package/src/astro-service/electional-service.ts +395 -0
  30. package/src/astro-service/natal-service.ts +235 -0
  31. package/src/astro-service/rising-sign-service.ts +181 -0
  32. package/src/astro-service/service-types.ts +90 -0
  33. package/src/astro-service/shared.ts +128 -0
  34. package/src/astro-service/sky-service.ts +191 -0
  35. package/src/astro-service/transit-service.ts +507 -0
  36. package/src/astro-service.ts +177 -1386
  37. package/src/tool-registry.ts +1 -1
  38. package/tests/README.md +15 -0
  39. package/tests/property/electional-service.property.test.ts +67 -0
  40. package/tests/property/helpers/arbitraries.ts +126 -0
  41. package/tests/property/helpers/config.ts +52 -0
  42. package/tests/property/helpers/runtime.ts +12 -0
  43. package/tests/property/houses.property.test.ts +74 -0
  44. package/tests/property/rising-sign-service.property.test.ts +255 -0
  45. package/tests/property/service-transits.property.test.ts +154 -0
  46. package/tests/property/time-utils.property.test.ts +91 -0
  47. package/tests/property/transits.property.test.ts +113 -0
  48. package/tests/unit/astro-service/chart-output-service.test.ts +102 -0
  49. package/tests/unit/astro-service/electional-service.test.ts +182 -0
  50. package/tests/unit/astro-service/natal-service.test.ts +126 -0
  51. package/tests/unit/astro-service/rising-sign-service.test.ts +145 -0
  52. package/tests/unit/astro-service/sky-service.test.ts +130 -0
  53. package/tests/unit/astro-service/transit-service.test.ts +312 -0
  54. package/tests/unit/astro-service.test.ts +136 -781
  55. package/tests/unit/rising-sign-windows.test.ts +93 -0
  56. package/tests/unit/tool-registry.test.ts +11 -0
  57. package/tests/validation/README.md +14 -0
  58. package/tests/validation/adapters/internal.ts +234 -4
  59. package/tests/validation/compare/electional.ts +151 -0
  60. package/tests/validation/compare/rising-sign-windows.ts +347 -0
  61. package/tests/validation/compare/service-transits.ts +205 -0
  62. package/tests/validation/fixtures/electional/core.ts +88 -0
  63. package/tests/validation/fixtures/rising-sign-windows/core.ts +57 -0
  64. package/tests/validation/fixtures/service-transits/core.ts +89 -0
  65. package/tests/validation/utils/fixtureTypes.ts +139 -1
  66. package/tests/validation/validation.spec.ts +82 -0
@@ -175,7 +175,7 @@ export const MCP_TOOL_SPECS = [
175
175
  },
176
176
  include_mundane: {
177
177
  type: 'boolean',
178
- description: 'Include current planetary positions (not transits to natal chart). Defaults to false.',
178
+ description: 'Include deterministic mundane baseline data for the requested window. Output includes planetary positions, transit-to-transit mundane aspects, and non-narrative weather grouping metadata; forecast windows also include per-day mundane.days entries. Defaults to false.',
179
179
  },
180
180
  days_ahead: {
181
181
  type: 'number',
@@ -195,6 +195,14 @@ Do not add a new MCP primitive when:
195
195
 
196
196
  Based on the current product direction:
197
197
 
198
+ ### Shared behavior boundary
199
+
200
+ - `src/astro-service.ts` remains the shared CLI/MCP behavior boundary.
201
+ - `AstroService` should stay a thin facade and orchestration layer.
202
+ - Shared implementation should usually live in the internal domain modules under `src/astro-service/`.
203
+ - New shared behavior should be added to the appropriate extracted service first, not piled back into the facade file.
204
+ - Public types may be re-exported from `src/astro-service.ts`, but domain logic should stay in the internal service modules unless there is a clear contract reason not to.
205
+
198
206
  ### Strong candidates for MCP
199
207
 
200
208
  - range-aware transit forecast data
@@ -0,0 +1,51 @@
1
+ # ether-to-astro v1.3.0
2
+
3
+ Suggested release type: `minor`
4
+
5
+ This release builds on `1.2.0` with a friendlier binary name, a cleaner internal service architecture, stronger validation/property coverage, and a small but explicit rising-sign payload correction. It also finishes the public contract alignment for the already-shipped mundane aspect and weather output.
6
+
7
+ ## 1.3.0 (2026-03-29)
8
+
9
+ ### New feature
10
+
11
+ - add `ether-to-astro` as a first-class CLI binary alias alongside `e2a` and `e2a-mcp`
12
+ - add a property-test lane via `npm run test:property` for generated invariants across time utilities, houses, transits, electional context, service-level transits, and rising-sign windows
13
+ - expand validation harness coverage to include service-layer electional context, rising-sign windows, and service transit serialization
14
+
15
+ ### Bug fix
16
+
17
+ - ⚠️ Breaking change: `getRisingSignWindows` structured window payloads now emit `durationMs` instead of `durationMinutes`
18
+ - use raw Sun altitude for electional sect classification so slightly negative values that round to `0.00` are still classified correctly as night charts
19
+ - keep rising-sign exact-vs-approximate precision checks active even when approximate windows only match a coarse subsequence of exact windows
20
+ - align the rising-sign property helper with the validation comparator so the generated invariant measures the first real departure from the left sign instead of assuming a direct sign-to-sign jump
21
+
22
+ ### Documentation Changes
23
+
24
+ - update `get_transits` contract wording so `include_mundane` describes the shipped mundane positions, aspects, weather metadata, and forecast `mundane.days[]` behavior
25
+ - refresh README transit docs so public docs no longer imply range-aware mundane output is still tracked separately
26
+
27
+ ### ⚙️ Chore
28
+
29
+ - extract transit, electional, rising-sign, natal, sky, and chart-output workflows into focused `src/astro-service/*` modules while preserving the shared `AstroService` surface
30
+ - finish migration to shared service types and reorganized test seams so service behavior is easier to validate without surface drift
31
+ - add service-layer validation coverage and a new `npm run test:property` lane for generated invariant checks across core astro and service behavior
32
+ - add regression coverage for the rising-sign precision no-op scenario and contract coverage for mundane schema wording drift
33
+
34
+ ## Upgrade notes
35
+
36
+ - 🚨 Breaking change: `getRisingSignWindows` now emits `durationMs` for each window object
37
+ - ❌ `durationMinutes` is no longer present in the structured rising-sign payload
38
+ - 🔧 if you consume rising-sign output programmatically, update any client code that reads or displays the old minute-based field
39
+ - 🧭 if you consume `include_mundane` programmatically, the docs/schema now reflect the richer behavior already shipping on `main`
40
+
41
+ ## Included PRs
42
+
43
+ - #44 fix electional sect classification to use raw Sun altitude
44
+ - #46 extract transit workflow into focused service layer modules
45
+ - #49 add `ether-to-astro` binary alias
46
+ - #50 extract electional and rising-sign workflows
47
+ - #51 extract natal, sky, and chart-output workflows
48
+ - #52 finish service docs/types/test migration
49
+ - #53 add service-layer validation coverage and enforce the `durationMs` contract
50
+ - #54 add property-based invariant coverage
51
+ - #55 keep rising-sign precision validation active and align mundane contract docs/schema with shipped behavior
@@ -0,0 +1,17 @@
1
+ # Releases
2
+
3
+ Release notes for `ether-to-astro` live in this directory.
4
+
5
+ Conventions:
6
+
7
+ - Use one draft file per pending release, named like `1.2.0-draft.md`.
8
+ - Call out breaking API or payload changes explicitly in an `Upgrade notes` section.
9
+ - If a breaking change is kept despite early release-stage flexibility, say so plainly instead of hiding it in feature bullets.
10
+
11
+ Current draft:
12
+
13
+ - [v1.2.0 draft](/Users/salted/Code/astro-mcp/docs/releases/1.2.0-draft.md)
14
+
15
+ Released notes:
16
+
17
+ - [v1.3.0 notes](/Users/salted/Code/astro-mcp/docs/releases/1.3.0.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ether-to-astro",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Local-first astrology toolkit with a unified e2a binary for CLI and MCP workflows, plus an e2a-mcp compatibility alias.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,7 @@
8
8
  "node": ">=22"
9
9
  },
10
10
  "bin": {
11
+ "ether-to-astro": "dist/loader.js",
11
12
  "e2a": "dist/loader.js",
12
13
  "e2a-mcp": "dist/mcp-alias.js"
13
14
  },
@@ -19,6 +20,7 @@
19
20
  "start:cli": "node dist/loader.js",
20
21
  "postinstall": "node scripts/download-ephemeris.js",
21
22
  "test": "vitest tests/unit",
23
+ "test:property": "vitest run tests/property",
22
24
  "validate:astro": "vitest run tests/validation",
23
25
  "test:ui": "vitest --ui",
24
26
  "test:coverage": "vitest run tests/unit --coverage",
@@ -56,6 +58,7 @@
56
58
  "@types/node": "^20.0.0",
57
59
  "@vitest/coverage-v8": "^4.1.2",
58
60
  "@vitest/ui": "^4.1.2",
61
+ "fast-check": "^4.6.0",
59
62
  "happy-dom": "^20.8.9",
60
63
  "tsx": "^4.21.0",
61
64
  "typescript": "^6.0.2",
@@ -0,0 +1,155 @@
1
+ import type { ChartRenderer } from '../charts.js';
2
+ import { getDefaultTheme } from '../constants.js';
3
+ import { formatDateOnly } from '../formatter.js';
4
+ import { localToUTC, utcToLocal } from '../time-utils.js';
5
+ import type { NatalChart } from '../types.js';
6
+ import { parseDateOnlyInput } from './date-input.js';
7
+ import type { GenerateChartInput, GenerateTransitChartInput } from './service-types.js';
8
+
9
+ interface ChartServiceResult {
10
+ format: 'svg' | 'png' | 'webp';
11
+ outputPath?: string;
12
+ text: string;
13
+ svg?: string;
14
+ image?: {
15
+ data: string;
16
+ mimeType: string;
17
+ };
18
+ }
19
+
20
+ interface ChartOutputServiceDependencies {
21
+ chartRenderer: ChartRenderer;
22
+ now: () => Date;
23
+ writeFile: (path: string, data: string | Buffer, encoding?: BufferEncoding) => Promise<void>;
24
+ }
25
+
26
+ /**
27
+ * Internal chart rendering/output workflow used by `AstroService`.
28
+ *
29
+ * @remarks
30
+ * This module owns theme defaults, target-date resolution, and inline-vs-file
31
+ * output serialization for natal and transit chart rendering.
32
+ */
33
+ export class ChartOutputService {
34
+ private readonly chartRenderer: ChartRenderer;
35
+ private readonly now: () => Date;
36
+ private readonly writeFile: (
37
+ path: string,
38
+ data: string | Buffer,
39
+ encoding?: BufferEncoding
40
+ ) => Promise<void>;
41
+
42
+ constructor(deps: ChartOutputServiceDependencies) {
43
+ this.chartRenderer = deps.chartRenderer;
44
+ this.now = deps.now;
45
+ this.writeFile = deps.writeFile;
46
+ }
47
+
48
+ /**
49
+ * Generate a natal chart image or SVG for the current chart.
50
+ */
51
+ async generateNatalChart(
52
+ natalChart: NatalChart,
53
+ input: GenerateChartInput = {}
54
+ ): Promise<ChartServiceResult> {
55
+ const theme = input.theme || getDefaultTheme(natalChart.location.timezone);
56
+ const format = input.format || 'svg';
57
+ const outputPath = input.output_path;
58
+ const chart = await this.chartRenderer.generateNatalChart(natalChart, theme, format);
59
+
60
+ if (outputPath) {
61
+ if (format === 'svg') {
62
+ await this.writeFile(outputPath, chart as string, 'utf-8');
63
+ } else {
64
+ await this.writeFile(outputPath, chart as Buffer);
65
+ }
66
+ return {
67
+ format,
68
+ outputPath,
69
+ text: `Natal Chart for ${natalChart.name} saved to: ${outputPath}`,
70
+ };
71
+ }
72
+
73
+ if (format === 'svg') {
74
+ return {
75
+ format,
76
+ text: `Natal Chart for ${natalChart.name}:`,
77
+ svg: chart as string,
78
+ };
79
+ }
80
+
81
+ return {
82
+ format,
83
+ text: `Natal Chart for ${natalChart.name} (${theme} theme, ${format.toUpperCase()} format):`,
84
+ image: {
85
+ data: (chart as Buffer).toString('base64'),
86
+ mimeType: format === 'png' ? 'image/png' : 'image/webp',
87
+ },
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Generate a transit chart image or SVG for a target date.
93
+ */
94
+ async generateTransitChart(
95
+ natalChart: NatalChart,
96
+ input: GenerateTransitChartInput = {}
97
+ ): Promise<ChartServiceResult> {
98
+ const theme = input.theme ?? getDefaultTheme(natalChart.location.timezone);
99
+ const format = input.format ?? 'svg';
100
+ const targetDate = this.resolveTransitTargetDate(natalChart, input.date);
101
+ const outputPath = input.output_path;
102
+ const chart = await this.chartRenderer.generateTransitChart(
103
+ natalChart,
104
+ targetDate,
105
+ theme,
106
+ format
107
+ );
108
+ const dateLabel = formatDateOnly(targetDate, natalChart.location.timezone);
109
+
110
+ if (outputPath) {
111
+ if (format === 'svg') {
112
+ await this.writeFile(outputPath, chart as string, 'utf-8');
113
+ } else {
114
+ await this.writeFile(outputPath, chart as Buffer);
115
+ }
116
+ return {
117
+ format,
118
+ outputPath,
119
+ text: `Transit Chart for ${natalChart.name} (${dateLabel}) saved to ${outputPath}`,
120
+ };
121
+ }
122
+
123
+ if (format === 'svg') {
124
+ return {
125
+ format,
126
+ text: `Transit Chart for ${natalChart.name} (${dateLabel})`,
127
+ svg: chart as string,
128
+ };
129
+ }
130
+
131
+ return {
132
+ format,
133
+ text: `Transit Chart for ${natalChart.name} (${dateLabel}, ${theme} theme, ${format.toUpperCase()} format):`,
134
+ image: {
135
+ data: (chart as Buffer).toString('base64'),
136
+ mimeType: format === 'png' ? 'image/png' : 'image/webp',
137
+ },
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Resolve the local-noon transit anchor when the caller omits a date.
143
+ */
144
+ private resolveTransitTargetDate(natalChart: NatalChart, dateStr?: string): Date {
145
+ if (dateStr) {
146
+ const parsed = parseDateOnlyInput(dateStr);
147
+ return localToUTC(parsed, natalChart.location.timezone);
148
+ }
149
+
150
+ const now = this.now();
151
+ const localNow = utcToLocal(now, natalChart.location.timezone);
152
+ const localNoon = { ...localNow, hour: 12, minute: 0, second: 0 };
153
+ return localToUTC(localNoon, natalChart.location.timezone);
154
+ }
155
+ }
@@ -0,0 +1,40 @@
1
+ import { Temporal } from '@js-temporal/polyfill';
2
+
3
+ /**
4
+ * Parse a date-only input into local noon components.
5
+ *
6
+ * @remarks
7
+ * The service treats date-only transit requests as local-noon lookups so the
8
+ * requested calendar day remains stable across timezone conversions.
9
+ */
10
+ export function parseDateOnlyInput(dateStr: string): {
11
+ year: number;
12
+ month: number;
13
+ day: number;
14
+ hour: number;
15
+ minute: number;
16
+ } {
17
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
18
+ if (!match) {
19
+ throw new Error(`Invalid date format: expected YYYY-MM-DD, got "${dateStr}"`);
20
+ }
21
+
22
+ const year = Number(match[1]);
23
+ const month = Number(match[2]);
24
+ const day = Number(match[3]);
25
+
26
+ if (month < 1 || month > 12) {
27
+ throw new Error(`Invalid month: ${month} (must be 1-12)`);
28
+ }
29
+ if (day < 1 || day > 31) {
30
+ throw new Error(`Invalid day: ${day} (must be 1-31)`);
31
+ }
32
+
33
+ try {
34
+ Temporal.PlainDate.from({ year, month, day });
35
+ } catch {
36
+ throw new Error(`Invalid calendar date: ${dateStr}`);
37
+ }
38
+
39
+ return { year, month, day, hour: 12, minute: 0 };
40
+ }