@openfate/bazi-engine 1.0.0 → 1.1.1

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 CHANGED
@@ -3,10 +3,10 @@
3
3
  ![NPM Version](https://img.shields.io/npm/v/@openfate/bazi-engine)
4
4
  ![License](https://img.shields.io/npm/l/@openfate/bazi-engine)
5
5
 
6
- > **The accurate, production-ready Four Pillars (八字) engine for JavaScript & TypeScript.**
6
+ > **A deterministic, production-oriented Four Pillars (八字) engine for JavaScript and TypeScript.**
7
7
  > Powered by [OpenFate.ai](https://openfate.ai) — the AI-native metaphysical analysis platform.
8
8
 
9
- Getting the Four Pillars right is hard. Solar Term boundaries (节气), True Solar Time correction (真太阳时), day-change boundaries, Lunar calendar conversion one wrong edge case and every chart is off. **We've solved all of it.**
9
+ Getting the Four Pillars right is hard. Solar Term boundaries (节气), True Solar Time correction (真太阳时), day-change boundaries, and lunar conversion all require explicit, testable policy. This package exposes those calculations and the metadata needed to audit them.
10
10
 
11
11
  ---
12
12
 
@@ -155,6 +155,10 @@ Main entry point. Returns a `BaziChart` with:
155
155
  - `daYun` — 9 Major Luck Cycles with start year/age
156
156
  - `interactions` — All detected branch interactions (7 types)
157
157
  - `solarTimeInfo` — True Solar Time details (or null if disabled)
158
+ - `calendar` — Civil solar input, calculation solar time, converted lunar date, and zodiac
159
+ - `metadata` — Applied True Solar Time, timezone, DST, and day-boundary policy
160
+
161
+ Each pillar preserves the simple `stem`, `branch`, and `element` fields and also includes Ten Gods, hidden stems, Na Yin, Xun, void branches, and growth stage.
158
162
 
159
163
  ### `detectInteractions(natal, annualBranch?): BranchInteraction[]`
160
164
  Detect interactions in a natal chart, optionally against an annual branch (太岁).
@@ -166,13 +170,15 @@ Low-level pillar generator — use when you've already handled time correction y
166
170
 
167
171
  ## 🛡️ Testing & Reliability
168
172
 
169
- Bazi calculations are notoriously prone to edge-case bugs. We maintain a **comprehensive regression suite** of 100+ global edge cases, verified against astronomical standards:
173
+ Bazi calculations are notoriously prone to edge-case bugs. We maintain a regression suite of 100+ calendrical cases plus focused public-contract tests:
170
174
 
171
175
  - **24 Solar Terms**: Minute-level precision for *Li Chun*, *Jing Zhe*, etc.
172
176
  - **Early/Late Zi Hour**: Handles the 23:00 day-rollover across month/year boundaries.
173
177
  - **Global Distortion**: Extreme longitudes (e.g., Xinjiang, Iceland) and fractional timezones (e.g., India UTC+5.5).
174
- - **Historical DST**: Automatic handling of Chinese DST (1986-1991), UK Double DST, and more.
175
- - **Century Boundaries**: Verified accuracy for dates across 1800, 1900, 2000, and 2100.
178
+ - **Historical DST**: Explicit `dstOffset` input or IANA timezone rules; the engine does not guess historical policy.
179
+ - **Century Boundaries**: Regression coverage for dates across 1800, 1900, 2000, and 2100.
180
+ - **Da Yun**: Exact start date, elapsed-age convention, direction, and cycle boundaries.
181
+ - **Factual Enrichment**: Ten Gods, hidden stems, Na Yin, Xun, void branches, and growth stages.
176
182
 
177
183
  Run the tests yourself:
178
184
  ```bash
@@ -201,6 +207,28 @@ Birth input (civil time)
201
207
 
202
208
  `@openfate/bazi-engine` gives you the data. **[OpenFate.ai](https://openfate.ai/bazi)** gives you the complete AI-powered interpretation — relationship analysis, career forecasting, and interactive AI chat with your chart.
203
209
 
210
+ ## GitHub Pages Site
211
+
212
+ This repository now includes a static GitHub Pages microsite in [`docs/index.html`](docs/index.html).
213
+
214
+ To publish it:
215
+
216
+ 1. Push the repository to GitHub on the `main` branch.
217
+ 2. In the repository settings, open `Pages`.
218
+ 3. Under `Build and deployment`, select `GitHub Actions`.
219
+ 4. Push a new commit or manually run the `Deploy GitHub Pages` workflow.
220
+
221
+ If the workflow fails with `Get Pages site failed`:
222
+
223
+ - The repository does not have GitHub Pages enabled yet.
224
+ - The fastest fix is to open `Settings` -> `Pages` and set `Source` to `GitHub Actions`, then rerun the workflow.
225
+ - Optional: create a repository secret named `PAGES_TOKEN` with a token that can administer Pages. The workflow will then try to enable Pages automatically on the first run.
226
+
227
+ Once enabled, the site will publish at:
228
+
229
+ - `https://openfate-ai.github.io/bazi-engine/` for the current repository name
230
+ - or `https://<owner>.github.io/<repo>/` for forks and renamed repositories
231
+
204
232
  ## License
205
233
 
206
234
  MIT — Free to use, modify, and distribute commercially.
@@ -4,6 +4,8 @@
4
4
  // ============================================================================
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.calculateDaYun = calculateDaYun;
7
+ const pillars_1 = require("./pillars");
8
+ const tenGods_1 = require("./tenGods");
7
9
  /**
8
10
  * calculateDaYun
9
11
  *
@@ -21,6 +23,7 @@ exports.calculateDaYun = calculateDaYun;
21
23
  function calculateDaYun(eightChar, gender, birthYear) {
22
24
  const genderInt = gender === 'male' ? 1 : 0;
23
25
  const yun = eightChar.getYun(genderInt);
26
+ const dayStem = eightChar.getDayGan();
24
27
  const rawCycles = yun.getDaYun();
25
28
  const cycles = [];
26
29
  // Index 0 is the natal chart segment — cycles start at index 1
@@ -31,6 +34,7 @@ function calculateDaYun(eightChar, gender, birthYear) {
31
34
  const stem = ganZhi.substring(0, 1);
32
35
  const branch = ganZhi.substring(1, 2);
33
36
  const startYear = dy.getStartYear();
37
+ const endYear = dy.getEndYear();
34
38
  cycles.push({
35
39
  index: i,
36
40
  stem,
@@ -38,12 +42,26 @@ function calculateDaYun(eightChar, gender, birthYear) {
38
42
  ganZhi,
39
43
  startYear,
40
44
  startAge: startYear - birthYear,
45
+ endYear,
46
+ endAge: endYear - birthYear,
47
+ stemTenGod: (0, tenGods_1.calculateTenGod)(dayStem, stem),
48
+ branchTenGod: (0, tenGods_1.calculateTenGod)(dayStem, (0, pillars_1.getMainQi)(branch)),
41
49
  });
42
50
  }
51
+ const firstCycle = cycles[0];
52
+ if (!firstCycle)
53
+ throw new Error('Unable to calculate Da Yun cycles.');
43
54
  return {
44
55
  cycles,
45
56
  isForward: yun.isForward(),
46
- startYear: yun.getStartYear(),
47
- startAge: yun.getStartYear() - birthYear,
57
+ startYear: firstCycle.startYear,
58
+ startAge: firstCycle.startAge,
59
+ startDate: yun.getStartSolar().toYmdHms(),
60
+ startOffset: {
61
+ years: yun.getStartYear(),
62
+ months: yun.getStartMonth(),
63
+ days: yun.getStartDay(),
64
+ hours: yun.getStartHour(),
65
+ },
48
66
  };
49
67
  }
@@ -59,8 +59,16 @@ function hasBranch(branches, target) {
59
59
  return branches.find(b => b.branch === target);
60
60
  }
61
61
  function groupHas(branches, group) {
62
- const found = group.map(b => hasBranch(branches, b)).filter(Boolean);
63
- return found.length === group.length ? found : null;
62
+ const remaining = [...branches];
63
+ const found = [];
64
+ for (const target of group) {
65
+ const index = remaining.findIndex(item => item.branch === target);
66
+ if (index === -1)
67
+ return null;
68
+ found.push(remaining[index]);
69
+ remaining.splice(index, 1);
70
+ }
71
+ return found;
64
72
  }
65
73
  // ── Main Detector ────────────────────────────────────────────────────────────
66
74
  /**
@@ -1,4 +1,4 @@
1
- import { FourPillars, DayBoundaryMode } from '../types';
1
+ import { FourPillars, DayBoundaryMode, StemInfo } from '../types';
2
2
  /** Typed interface for the EightChar object returned by lunar-javascript */
3
3
  export interface LunarEightChar {
4
4
  setSect(sect: number): void;
@@ -10,6 +10,30 @@ export interface LunarEightChar {
10
10
  getDayZhi(): string;
11
11
  getTimeGan(): string;
12
12
  getTimeZhi(): string;
13
+ getYearNaYin(): string;
14
+ getYearShiShenGan(): string;
15
+ getYearShiShenZhi(): string[];
16
+ getYearDiShi(): string;
17
+ getYearXun(): string;
18
+ getYearXunKong(): string;
19
+ getMonthNaYin(): string;
20
+ getMonthShiShenGan(): string;
21
+ getMonthShiShenZhi(): string[];
22
+ getMonthDiShi(): string;
23
+ getMonthXun(): string;
24
+ getMonthXunKong(): string;
25
+ getDayNaYin(): string;
26
+ getDayShiShenGan(): string;
27
+ getDayShiShenZhi(): string[];
28
+ getDayDiShi(): string;
29
+ getDayXun(): string;
30
+ getDayXunKong(): string;
31
+ getTimeNaYin(): string;
32
+ getTimeShiShenGan(): string;
33
+ getTimeShiShenZhi(): string[];
34
+ getTimeDiShi(): string;
35
+ getTimeXun(): string;
36
+ getTimeXunKong(): string;
13
37
  getYun(gender: number): LunarYun;
14
38
  }
15
39
  /** Typed interface for the Yun (Luck Cycles) manager */
@@ -18,13 +42,20 @@ export interface LunarYun {
18
42
  getStartYear(): number;
19
43
  getStartMonth(): number;
20
44
  getStartDay(): number;
45
+ getStartHour(): number;
46
+ getStartSolar(): LunarSolar;
21
47
  getDaYun(): LunarDaYun[];
22
48
  }
49
+ export interface LunarSolar {
50
+ toYmdHms(): string;
51
+ }
23
52
  /** Typed interface for a single Da Yun period */
24
53
  export interface LunarDaYun {
25
54
  getGanZhi(): string;
26
55
  getStartYear(): number;
27
56
  getEndYear(): number;
57
+ getStartAge(): number;
58
+ getEndAge(): number;
28
59
  }
29
60
  export interface PillarResult {
30
61
  pillars: FourPillars;
@@ -53,9 +84,4 @@ export declare function getMainQi(branch: string): string;
53
84
  /**
54
85
  * getStemInfo - Returns enriched stem details
55
86
  */
56
- export declare function getStemInfo(stem: string): {
57
- char: string;
58
- pinyin: string;
59
- element: import("../types").FiveElement;
60
- polarity: import("../types").Polarity;
61
- };
87
+ export declare function getStemInfo(stem: string): StemInfo;
@@ -8,12 +8,41 @@ exports.getMainQi = getMainQi;
8
8
  exports.getStemInfo = getStemInfo;
9
9
  const lunar_javascript_1 = require("lunar-javascript");
10
10
  const constants_1 = require("../constants");
11
- function buildPillar(stem, branch) {
12
- var _a;
11
+ function getRequiredElement(value, mapping, label) {
12
+ const result = mapping[value];
13
+ if (!result)
14
+ throw new Error(`Unsupported ${label}: ${value}`);
15
+ return result;
16
+ }
17
+ function getRequiredPolarity(stem) {
18
+ const polarity = constants_1.STEM_TO_POLARITY[stem];
19
+ if (!polarity)
20
+ throw new Error(`Unsupported Heavenly Stem: ${stem}`);
21
+ return polarity;
22
+ }
23
+ function buildPillar(stem, branch, details) {
24
+ const hiddenStemDefinitions = constants_1.BRANCH_HIDDEN_STEMS[branch];
25
+ if (!hiddenStemDefinitions)
26
+ throw new Error(`Unsupported Earthly Branch: ${branch}`);
13
27
  return {
14
28
  stem,
15
29
  branch,
16
- element: (_a = constants_1.STEM_TO_ELEMENT[stem]) !== null && _a !== void 0 ? _a : 'earth',
30
+ element: getRequiredElement(stem, constants_1.STEM_TO_ELEMENT, 'Heavenly Stem'),
31
+ ganZhi: `${stem}${branch}`,
32
+ stemPolarity: getRequiredPolarity(stem),
33
+ stemTenGod: details.stemTenGod,
34
+ branchElement: getRequiredElement(branch, constants_1.BRANCH_TO_ELEMENT, 'Earthly Branch'),
35
+ hiddenStems: hiddenStemDefinitions.map((hiddenStem, index) => ({
36
+ stem: hiddenStem.stem,
37
+ element: getRequiredElement(hiddenStem.stem, constants_1.STEM_TO_ELEMENT, 'Hidden Stem'),
38
+ polarity: getRequiredPolarity(hiddenStem.stem),
39
+ tenGod: details.hiddenStemTenGods[index],
40
+ isMain: Boolean(hiddenStem.isMain),
41
+ })),
42
+ naYin: details.naYin,
43
+ xun: details.xun,
44
+ voidBranches: details.voidBranches.split(''),
45
+ growthStage: details.growthStage,
17
46
  };
18
47
  }
19
48
  /**
@@ -46,10 +75,38 @@ function generatePillarsFromSolar(year, month, day, hour, minute, second, dayBou
46
75
  const dayStem = eightChar.getDayGan();
47
76
  return {
48
77
  pillars: {
49
- year: buildPillar(eightChar.getYearGan(), eightChar.getYearZhi()),
50
- month: buildPillar(eightChar.getMonthGan(), eightChar.getMonthZhi()),
51
- day: buildPillar(dayStem, eightChar.getDayZhi()),
52
- hour: hasTime ? buildPillar(eightChar.getTimeGan(), eightChar.getTimeZhi()) : null,
78
+ year: buildPillar(eightChar.getYearGan(), eightChar.getYearZhi(), {
79
+ stemTenGod: eightChar.getYearShiShenGan(),
80
+ hiddenStemTenGods: eightChar.getYearShiShenZhi(),
81
+ naYin: eightChar.getYearNaYin(),
82
+ xun: eightChar.getYearXun(),
83
+ voidBranches: eightChar.getYearXunKong(),
84
+ growthStage: eightChar.getYearDiShi(),
85
+ }),
86
+ month: buildPillar(eightChar.getMonthGan(), eightChar.getMonthZhi(), {
87
+ stemTenGod: eightChar.getMonthShiShenGan(),
88
+ hiddenStemTenGods: eightChar.getMonthShiShenZhi(),
89
+ naYin: eightChar.getMonthNaYin(),
90
+ xun: eightChar.getMonthXun(),
91
+ voidBranches: eightChar.getMonthXunKong(),
92
+ growthStage: eightChar.getMonthDiShi(),
93
+ }),
94
+ day: buildPillar(dayStem, eightChar.getDayZhi(), {
95
+ stemTenGod: eightChar.getDayShiShenGan(),
96
+ hiddenStemTenGods: eightChar.getDayShiShenZhi(),
97
+ naYin: eightChar.getDayNaYin(),
98
+ xun: eightChar.getDayXun(),
99
+ voidBranches: eightChar.getDayXunKong(),
100
+ growthStage: eightChar.getDayDiShi(),
101
+ }),
102
+ hour: hasTime ? buildPillar(eightChar.getTimeGan(), eightChar.getTimeZhi(), {
103
+ stemTenGod: eightChar.getTimeShiShenGan(),
104
+ hiddenStemTenGods: eightChar.getTimeShiShenZhi(),
105
+ naYin: eightChar.getTimeNaYin(),
106
+ xun: eightChar.getTimeXun(),
107
+ voidBranches: eightChar.getTimeXunKong(),
108
+ growthStage: eightChar.getTimeDiShi(),
109
+ }) : null,
53
110
  },
54
111
  eightChar,
55
112
  dayStem,
@@ -68,11 +125,10 @@ function getMainQi(branch) {
68
125
  * getStemInfo - Returns enriched stem details
69
126
  */
70
127
  function getStemInfo(stem) {
71
- var _a, _b, _c;
72
128
  return {
73
129
  char: stem,
74
- pinyin: (_a = constants_1.STEM_TO_PINYIN[stem]) !== null && _a !== void 0 ? _a : '',
75
- element: (_b = constants_1.STEM_TO_ELEMENT[stem]) !== null && _b !== void 0 ? _b : 'earth',
76
- polarity: (_c = constants_1.STEM_TO_POLARITY[stem]) !== null && _c !== void 0 ? _c : 'yang',
130
+ pinyin: constants_1.STEM_TO_PINYIN[stem],
131
+ element: getRequiredElement(stem, constants_1.STEM_TO_ELEMENT, 'Heavenly Stem'),
132
+ polarity: getRequiredPolarity(stem),
77
133
  };
78
134
  }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * calculateTenGod - Returns the classical Ten God relationship between two stems.
3
+ */
4
+ export declare function calculateTenGod(dayMaster: string, targetStem: string): string;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateTenGod = calculateTenGod;
4
+ const constants_1 = require("../constants");
5
+ const ELEMENTS = ['wood', 'fire', 'earth', 'metal', 'water'];
6
+ const TEN_GOD_NAMES = {
7
+ self: { same: '比肩', different: '劫财' },
8
+ output: { same: '食神', different: '伤官' },
9
+ wealth: { same: '偏财', different: '正财' },
10
+ power: { same: '七杀', different: '正官' },
11
+ resource: { same: '偏印', different: '正印' },
12
+ };
13
+ function getRelationship(dayMaster, target) {
14
+ const dayMasterIndex = ELEMENTS.indexOf(dayMaster);
15
+ const targetIndex = ELEMENTS.indexOf(target);
16
+ const difference = (targetIndex - dayMasterIndex + ELEMENTS.length) % ELEMENTS.length;
17
+ if (difference === 0)
18
+ return 'self';
19
+ if (difference === 1)
20
+ return 'output';
21
+ if (difference === 2)
22
+ return 'wealth';
23
+ if (difference === 3)
24
+ return 'power';
25
+ return 'resource';
26
+ }
27
+ /**
28
+ * calculateTenGod - Returns the classical Ten God relationship between two stems.
29
+ */
30
+ function calculateTenGod(dayMaster, targetStem) {
31
+ const dayMasterElement = constants_1.STEM_TO_ELEMENT[dayMaster];
32
+ const targetElement = constants_1.STEM_TO_ELEMENT[targetStem];
33
+ const dayMasterPolarity = constants_1.STEM_TO_POLARITY[dayMaster];
34
+ const targetPolarity = constants_1.STEM_TO_POLARITY[targetStem];
35
+ if (!dayMasterElement || !dayMasterPolarity)
36
+ throw new Error(`Unsupported Day Master: ${dayMaster}`);
37
+ if (!targetElement || !targetPolarity)
38
+ throw new Error(`Unsupported target stem: ${targetStem}`);
39
+ const relationship = getRelationship(dayMasterElement, targetElement);
40
+ const names = TEN_GOD_NAMES[relationship];
41
+ return dayMasterPolarity === targetPolarity ? names.same : names.different;
42
+ }
@@ -0,0 +1,8 @@
1
+ import { BaziInput } from '../types';
2
+ export declare class BaziInputError extends RangeError {
3
+ constructor(message: string);
4
+ }
5
+ /**
6
+ * validateBaziInput - Validates the public calculation contract before invoking calendar libraries.
7
+ */
8
+ export declare function validateBaziInput(input: BaziInput): void;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaziInputError = void 0;
4
+ exports.validateBaziInput = validateBaziInput;
5
+ const lunar_javascript_1 = require("lunar-javascript");
6
+ const MIN_YEAR = 1800;
7
+ const MAX_YEAR = 2100;
8
+ class BaziInputError extends RangeError {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'BaziInputError';
12
+ }
13
+ }
14
+ exports.BaziInputError = BaziInputError;
15
+ function assertIntegerInRange(value, minimum, maximum, field) {
16
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
17
+ throw new BaziInputError(`${field} must be an integer between ${minimum} and ${maximum}.`);
18
+ }
19
+ }
20
+ function assertNumberInRange(value, minimum, maximum, field) {
21
+ if (!Number.isFinite(value) || value < minimum || value > maximum) {
22
+ throw new BaziInputError(`${field} must be between ${minimum} and ${maximum}.`);
23
+ }
24
+ }
25
+ function assertSolarDate(input) {
26
+ const date = new Date(Date.UTC(input.year, input.month - 1, input.day));
27
+ const isSameDate = date.getUTCFullYear() === input.year
28
+ && date.getUTCMonth() === input.month - 1
29
+ && date.getUTCDate() === input.day;
30
+ if (!isSameDate)
31
+ throw new BaziInputError('The supplied solar date does not exist.');
32
+ if (input.isLeapMonth)
33
+ throw new BaziInputError('isLeapMonth is only valid for lunar calendar input.');
34
+ }
35
+ function assertLunarDate(input) {
36
+ const lunarYear = lunar_javascript_1.LunarYear.fromYear(input.year);
37
+ const requestedMonth = input.isLeapMonth ? -input.month : input.month;
38
+ const lunarMonth = lunarYear.getMonth(requestedMonth);
39
+ if (!lunarMonth) {
40
+ const label = input.isLeapMonth ? 'leap lunar month' : 'lunar month';
41
+ throw new BaziInputError(`The supplied ${label} does not exist in ${input.year}.`);
42
+ }
43
+ if (input.day > lunarMonth.getDayCount()) {
44
+ throw new BaziInputError('The supplied lunar date does not exist.');
45
+ }
46
+ }
47
+ /**
48
+ * validateBaziInput - Validates the public calculation contract before invoking calendar libraries.
49
+ */
50
+ function validateBaziInput(input) {
51
+ var _a, _b;
52
+ assertIntegerInRange(input.year, MIN_YEAR, MAX_YEAR, 'year');
53
+ assertIntegerInRange(input.month, 1, 12, 'month');
54
+ assertIntegerInRange(input.day, 1, 31, 'day');
55
+ if (input.hour !== undefined)
56
+ assertIntegerInRange(input.hour, 0, 23, 'hour');
57
+ if (input.minute !== undefined)
58
+ assertIntegerInRange(input.minute, 0, 59, 'minute');
59
+ if (input.minute !== undefined && input.hour === undefined) {
60
+ throw new BaziInputError('hour is required when minute is supplied.');
61
+ }
62
+ if (input.longitude !== undefined)
63
+ assertNumberInRange(input.longitude, -180, 180, 'longitude');
64
+ if (input.timezone !== undefined)
65
+ assertNumberInRange(input.timezone, -14, 14, 'timezone');
66
+ if (input.dstOffset !== undefined)
67
+ assertNumberInRange(input.dstOffset, -2, 2, 'dstOffset');
68
+ if (input.timezoneId !== undefined && input.timezoneId.trim().length === 0) {
69
+ throw new BaziInputError('timezoneId cannot be empty.');
70
+ }
71
+ const trueSolarTimeRequested = ((_a = input.enableTrueSolarTime) !== null && _a !== void 0 ? _a : true)
72
+ && input.longitude !== undefined
73
+ && input.hour !== undefined;
74
+ if (trueSolarTimeRequested && input.timezone === undefined && input.timezoneId === undefined) {
75
+ throw new BaziInputError('timezone or timezoneId is required for True Solar Time correction.');
76
+ }
77
+ if (((_b = input.calendarType) !== null && _b !== void 0 ? _b : 'solar') === 'lunar') {
78
+ assertLunarDate(input);
79
+ }
80
+ else {
81
+ assertSolarDate(input);
82
+ }
83
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export * from './constants';
4
4
  export { detectInteractions } from './core/interactions';
5
5
  export { generatePillarsFromSolar, getStemInfo, getMainQi } from './core/pillars';
6
6
  export { calculateDaYun } from './core/cycles';
7
+ export { calculateTenGod } from './core/tenGods';
8
+ export { BaziInputError, validateBaziInput } from './core/validation';
7
9
  /**
8
10
  * calculateBaziChart
9
11
  *
package/dist/index.js CHANGED
@@ -17,13 +17,14 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
17
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.calculateDaYun = exports.getMainQi = exports.getStemInfo = exports.generatePillarsFromSolar = exports.detectInteractions = void 0;
20
+ exports.validateBaziInput = exports.BaziInputError = exports.calculateTenGod = exports.calculateDaYun = exports.getMainQi = exports.getStemInfo = exports.generatePillarsFromSolar = exports.detectInteractions = void 0;
21
21
  exports.calculateBaziChart = calculateBaziChart;
22
22
  const true_solar_time_1 = require("@openfate/true-solar-time");
23
23
  const lunar_javascript_1 = require("lunar-javascript");
24
24
  const pillars_1 = require("./core/pillars");
25
25
  const cycles_1 = require("./core/cycles");
26
26
  const interactions_1 = require("./core/interactions");
27
+ const validation_1 = require("./core/validation");
27
28
  // Re-export all public types and constants
28
29
  __exportStar(require("./types"), exports);
29
30
  __exportStar(require("./constants"), exports);
@@ -35,6 +36,41 @@ Object.defineProperty(exports, "getStemInfo", { enumerable: true, get: function
35
36
  Object.defineProperty(exports, "getMainQi", { enumerable: true, get: function () { return pillars_2.getMainQi; } });
36
37
  var cycles_2 = require("./core/cycles");
37
38
  Object.defineProperty(exports, "calculateDaYun", { enumerable: true, get: function () { return cycles_2.calculateDaYun; } });
39
+ var tenGods_1 = require("./core/tenGods");
40
+ Object.defineProperty(exports, "calculateTenGod", { enumerable: true, get: function () { return tenGods_1.calculateTenGod; } });
41
+ var validation_2 = require("./core/validation");
42
+ Object.defineProperty(exports, "BaziInputError", { enumerable: true, get: function () { return validation_2.BaziInputError; } });
43
+ Object.defineProperty(exports, "validateBaziInput", { enumerable: true, get: function () { return validation_2.validateBaziInput; } });
44
+ function createDateTime(year, month, day, hour, minute, second) {
45
+ const hasTime = hour !== undefined;
46
+ return {
47
+ year,
48
+ month,
49
+ day,
50
+ hour: hasTime ? hour : null,
51
+ minute: hasTime ? minute !== null && minute !== void 0 ? minute : 0 : null,
52
+ second: hasTime ? second !== null && second !== void 0 ? second : 0 : null,
53
+ };
54
+ }
55
+ function createLunarDateTime(solarDateTime) {
56
+ const solar = solarDateTime.hour === null
57
+ ? lunar_javascript_1.Solar.fromYmd(solarDateTime.year, solarDateTime.month, solarDateTime.day)
58
+ : lunar_javascript_1.Solar.fromYmdHms(solarDateTime.year, solarDateTime.month, solarDateTime.day, solarDateTime.hour, solarDateTime.minute, solarDateTime.second);
59
+ const lunar = solar.getLunar();
60
+ const lunarMonth = lunar.getMonth();
61
+ return {
62
+ lunar: {
63
+ year: lunar.getYear(),
64
+ month: Math.abs(lunarMonth),
65
+ day: lunar.getDay(),
66
+ hour: solarDateTime.hour,
67
+ minute: solarDateTime.minute,
68
+ second: solarDateTime.second,
69
+ isLeapMonth: lunarMonth < 0,
70
+ },
71
+ zodiac: lunar.getYearShengXiao(),
72
+ };
73
+ }
38
74
  /**
39
75
  * calculateBaziChart
40
76
  *
@@ -56,7 +92,8 @@ Object.defineProperty(exports, "calculateDaYun", { enumerable: true, get: functi
56
92
  * ```
57
93
  */
58
94
  function calculateBaziChart(input) {
59
- var _a, _b, _c, _d, _e, _f;
95
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
96
+ (0, validation_1.validateBaziInput)(input);
60
97
  let solarYear = input.year;
61
98
  let solarMonth = input.month;
62
99
  let solarDay = input.day;
@@ -64,6 +101,7 @@ function calculateBaziChart(input) {
64
101
  let solarMinute = (_a = input.minute) !== null && _a !== void 0 ? _a : 0;
65
102
  let solarSecond = 0;
66
103
  let solarTimeInfo = null;
104
+ const inputType = (_b = input.calendarType) !== null && _b !== void 0 ? _b : 'solar';
67
105
  // ── 1. Lunar → Solar Calendar Conversion ────────────────────────────────
68
106
  if (input.calendarType === 'lunar') {
69
107
  // lunar-javascript: negative month represents a leap month
@@ -74,18 +112,31 @@ function calculateBaziChart(input) {
74
112
  solarMonth = solar.getMonth();
75
113
  solarDay = solar.getDay();
76
114
  }
115
+ const civilSolar = createDateTime(solarYear, solarMonth, solarDay, input.hour, input.hour === undefined ? undefined : solarMinute, input.hour === undefined ? undefined : solarSecond);
77
116
  // ── 2. True Solar Time Correction ───────────────────────────────────────
78
- const enableTST = ((_b = input.enableTrueSolarTime) !== null && _b !== void 0 ? _b : true) &&
117
+ const enableTST = ((_c = input.enableTrueSolarTime) !== null && _c !== void 0 ? _c : true) &&
79
118
  input.longitude !== undefined && input.longitude !== null &&
80
119
  input.hour !== undefined;
81
120
  if (enableTST) {
82
- const detail = (0, true_solar_time_1.calculateTrueSolarTime)({
83
- year: solarYear, month: solarMonth, day: solarDay,
84
- hour: solarHour, minute: solarMinute,
85
- timeZoneOffset: input.timezone,
86
- dstOffset: (_c = input.dstOffset) !== null && _c !== void 0 ? _c : 0,
87
- timeZoneId: input.timezoneId,
88
- }, {
121
+ const civilTimeInput = input.timezone !== undefined
122
+ ? {
123
+ year: solarYear,
124
+ month: solarMonth,
125
+ day: solarDay,
126
+ hour: solarHour,
127
+ minute: solarMinute,
128
+ timeZoneOffset: input.timezone,
129
+ dstOffset: (_d = input.dstOffset) !== null && _d !== void 0 ? _d : 0,
130
+ }
131
+ : {
132
+ year: solarYear,
133
+ month: solarMonth,
134
+ day: solarDay,
135
+ hour: solarHour,
136
+ minute: solarMinute,
137
+ timeZoneId: input.timezoneId,
138
+ };
139
+ const detail = (0, true_solar_time_1.calculateTrueSolarTime)(civilTimeInput, {
89
140
  longitude: input.longitude,
90
141
  });
91
142
  const [dateStr, timeStr] = detail.trueSolarDateTime.split(' ');
@@ -101,13 +152,32 @@ function calculateBaziChart(input) {
101
152
  trueSolarTime: detail.trueSolarTime.substring(0, 5),
102
153
  trueSolarDateTime: detail.trueSolarDateTime,
103
154
  solarDate: dateStr,
155
+ standardMeridian: detail.standardMeridian,
104
156
  longitudeCorrectionMinutes: detail.longitudeCorrectionMinutes,
105
157
  equationOfTimeMinutes: detail.equationOfTimeMinutes,
106
158
  algorithm: detail.algorithm,
107
159
  };
108
160
  }
161
+ else if (((_e = input.dstOffset) !== null && _e !== void 0 ? _e : 0) !== 0 && input.hour !== undefined) {
162
+ // ── 2b. DST normalization without True Solar Time ────────────────────
163
+ // DST is a civil-clock correction, not a solar-time refinement: a birth
164
+ // recorded at 15:20 during a DST period (e.g. China 1986–1991 summer,
165
+ // Taiwan 1946–1961) actually occurred at 14:20 standard time. The TST
166
+ // path above already subtracts dstOffset inside calculateTrueSolarTime;
167
+ // when TST is disabled it must still be normalized out here, otherwise
168
+ // the pillars use the shifted clock hour — a wrong 时辰, and near
169
+ // midnight a wrong day pillar. Date-based math keeps day/month/year
170
+ // rollover safe (lunar input was already converted to solar above).
171
+ const shifted = new Date(Date.UTC(solarYear, solarMonth - 1, solarDay, solarHour, solarMinute));
172
+ shifted.setUTCMinutes(shifted.getUTCMinutes() - Math.round(((_f = input.dstOffset) !== null && _f !== void 0 ? _f : 0) * 60));
173
+ solarYear = shifted.getUTCFullYear();
174
+ solarMonth = shifted.getUTCMonth() + 1;
175
+ solarDay = shifted.getUTCDate();
176
+ solarHour = shifted.getUTCHours();
177
+ solarMinute = shifted.getUTCMinutes();
178
+ }
109
179
  // ── 3. Generate Four Pillars ─────────────────────────────────────────────
110
- const { pillars, eightChar, dayStem } = (0, pillars_1.generatePillarsFromSolar)(solarYear, solarMonth, solarDay, solarHour, solarMinute, solarSecond, (_d = input.dayBoundaryMode) !== null && _d !== void 0 ? _d : 'MIDNIGHT_00');
180
+ const { pillars, eightChar, dayStem } = (0, pillars_1.generatePillarsFromSolar)(solarYear, solarMonth, solarDay, solarHour, solarMinute, solarSecond, (_g = input.dayBoundaryMode) !== null && _g !== void 0 ? _g : 'MIDNIGHT_00');
111
181
  // ── 4. Day Master ────────────────────────────────────────────────────────
112
182
  const dayMaster = (0, pillars_1.getStemInfo)(dayStem);
113
183
  // ── 5. Da Yun Cycles ─────────────────────────────────────────────────────
@@ -117,7 +187,29 @@ function calculateBaziChart(input) {
117
187
  year: pillars.year.branch,
118
188
  month: pillars.month.branch,
119
189
  day: pillars.day.branch,
120
- hour: (_f = (_e = pillars.hour) === null || _e === void 0 ? void 0 : _e.branch) !== null && _f !== void 0 ? _f : '',
190
+ hour: (_j = (_h = pillars.hour) === null || _h === void 0 ? void 0 : _h.branch) !== null && _j !== void 0 ? _j : '',
121
191
  });
122
- return { pillars, dayMaster, daYun, interactions, solarTimeInfo };
192
+ const calculationSolar = createDateTime(solarYear, solarMonth, solarDay, input.hour === undefined ? undefined : solarHour, input.hour === undefined ? undefined : solarMinute, input.hour === undefined ? undefined : solarSecond);
193
+ const lunarCalendar = createLunarDateTime(calculationSolar);
194
+ return {
195
+ pillars,
196
+ dayMaster,
197
+ daYun,
198
+ interactions,
199
+ solarTimeInfo,
200
+ calendar: {
201
+ inputType,
202
+ civilSolar,
203
+ calculationSolar,
204
+ lunar: lunarCalendar.lunar,
205
+ zodiac: lunarCalendar.zodiac,
206
+ },
207
+ metadata: {
208
+ trueSolarTimeApplied: enableTST,
209
+ dayBoundaryMode: (_k = input.dayBoundaryMode) !== null && _k !== void 0 ? _k : 'MIDNIGHT_00',
210
+ longitude: (_l = input.longitude) !== null && _l !== void 0 ? _l : null,
211
+ timezoneBasis: (_o = (_m = input.timezone) !== null && _m !== void 0 ? _m : input.timezoneId) !== null && _o !== void 0 ? _o : null,
212
+ dstOffset: (_p = input.dstOffset) !== null && _p !== void 0 ? _p : 0,
213
+ },
214
+ };
123
215
  }
package/dist/types.d.ts CHANGED
@@ -20,6 +20,22 @@ export interface Pillar {
20
20
  stem: string;
21
21
  branch: string;
22
22
  element: FiveElement;
23
+ ganZhi: string;
24
+ stemPolarity: Polarity;
25
+ stemTenGod: string;
26
+ branchElement: FiveElement;
27
+ hiddenStems: HiddenStemInfo[];
28
+ naYin: string;
29
+ xun: string;
30
+ voidBranches: string[];
31
+ growthStage: string;
32
+ }
33
+ export interface HiddenStemInfo {
34
+ stem: string;
35
+ element: FiveElement;
36
+ polarity: Polarity;
37
+ tenGod: string;
38
+ isMain: boolean;
23
39
  }
24
40
  /** The Four Pillars (四柱) */
25
41
  export interface FourPillars {
@@ -36,6 +52,10 @@ export interface DaYunCycle {
36
52
  ganZhi: string;
37
53
  startYear: number;
38
54
  startAge: number;
55
+ endYear: number;
56
+ endAge: number;
57
+ stemTenGod: string;
58
+ branchTenGod: string;
39
59
  }
40
60
  /** Da Yun metadata */
41
61
  export interface DaYunInfo {
@@ -43,6 +63,13 @@ export interface DaYunInfo {
43
63
  isForward: boolean;
44
64
  startYear: number;
45
65
  startAge: number;
66
+ startDate: string;
67
+ startOffset: {
68
+ years: number;
69
+ months: number;
70
+ days: number;
71
+ hours: number;
72
+ };
46
73
  }
47
74
  export type InteractionType = 'CLASH' | 'COMBINATION_2' | 'TRINE' | 'DIRECTIONAL' | 'PUNISHMENT' | 'DESTRUCTION' | 'HARM';
48
75
  /** A detected branch interaction */
@@ -57,10 +84,36 @@ export interface SolarTimeInfo {
57
84
  trueSolarTime: string;
58
85
  trueSolarDateTime: string;
59
86
  solarDate: string;
87
+ standardMeridian: number;
60
88
  longitudeCorrectionMinutes: number;
61
89
  equationOfTimeMinutes: number;
62
90
  algorithm: string;
63
91
  }
92
+ export interface CalendarDateTime {
93
+ year: number;
94
+ month: number;
95
+ day: number;
96
+ hour: number | null;
97
+ minute: number | null;
98
+ second: number | null;
99
+ }
100
+ export interface LunarDateTime extends CalendarDateTime {
101
+ isLeapMonth: boolean;
102
+ }
103
+ export interface CalendarInfo {
104
+ inputType: 'solar' | 'lunar';
105
+ civilSolar: CalendarDateTime;
106
+ calculationSolar: CalendarDateTime;
107
+ lunar: LunarDateTime;
108
+ zodiac: string;
109
+ }
110
+ export interface CalculationMetadata {
111
+ trueSolarTimeApplied: boolean;
112
+ dayBoundaryMode: DayBoundaryMode;
113
+ longitude: number | null;
114
+ timezoneBasis: number | string | null;
115
+ dstOffset: number;
116
+ }
64
117
  /** Full output of calculateBaziChart() */
65
118
  export interface BaziChart {
66
119
  pillars: FourPillars;
@@ -68,6 +121,8 @@ export interface BaziChart {
68
121
  daYun: DaYunInfo;
69
122
  interactions: BranchInteraction[];
70
123
  solarTimeInfo: SolarTimeInfo | null;
124
+ calendar: CalendarInfo;
125
+ metadata: CalculationMetadata;
71
126
  }
72
127
  export interface BaziInput {
73
128
  year: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfate/bazi-engine",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Accurate Four Pillars (八字) chart engine with True Solar Time correction, Da Yun cycles, and interaction detection. Extracted from the OpenFate.ai core engine.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "build": "tsc",
14
- "test": "tsx --test tests/*.test.ts",
14
+ "test": "node --import tsx --test tests/*.test.ts",
15
15
  "release": "npm version patch && git push --follow-tags",
16
16
  "prepublishOnly": "npm run build && npm run test"
17
17
  },