dsh-metaphysics 0.34.0 → 0.46.5

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/engine/ziwei.js CHANGED
@@ -74,12 +74,182 @@ export const MUTAGEN = {
74
74
  癸: { 破军: '禄', 巨门: '权', 太阴: '科', 贪狼: '忌' },
75
75
  };
76
76
 
77
+ // ── 流派开关 ──────────────────────────────────────────────────
78
+ // 默认值对齐本插件既有盘面:亮度用内置通行表,庚干用阳武阴同,
79
+ // 天马按年支、魁钺用全书理解一、天使天伤不换、长生按现有五行局表、
80
+ // 晚子时仍算当日;天空/截空旬空默认不排(旧盘没有这两组星,保持 49 颗)。
81
+ // 其余选项只在显式指定时改变盘面,不影响默认输出。
82
+ export const ZIWEI_SCHOOL_DEFAULTS = Object.freeze({
83
+ brightnessSchool: 'default',
84
+ gengYearSihua: 'GYWYT',
85
+ tianmaSchool: 'year-branch',
86
+ tiankongSchool: 'off',
87
+ jiekongSchool: 'off',
88
+ kuiyueSchool: 'qs1',
89
+ tianshiTianshang: 'normal',
90
+ changshengSchool: 'yin-yang-shun-ni',
91
+ lateZishi: 'current-day',
92
+ });
93
+
94
+ export const ZIWEI_SCHOOL_OPTIONS = Object.freeze({
95
+ brightnessSchool: ['default', 'QS', 'XD1', 'ZZ', 'XD2'],
96
+ gengYearSihua: ['GYWYT', 'GYWTY', 'GYWFT', 'GYWFX', 'GYWTX'],
97
+ tianmaSchool: ['year-branch', 'month-branch'],
98
+ tiankongSchool: ['off', 'normal', 'shun-jia-shi'],
99
+ jiekongSchool: ['off', 'double', 'single', 'zhanyan'],
100
+ kuiyueSchool: ['qs1', 'qs2', 'zhong-yiming', 'network'],
101
+ tianshiTianshang: ['normal', 'zhongzhou'],
102
+ changshengSchool: ['yin-yang-shun-ni', 'shui-tu', 'huo-tu'],
103
+ lateZishi: ['current-day', 'next-day', 'all-current', 'day-cur-time-next'],
104
+ });
105
+
106
+ export function ziweiSchoolsOf(input = {}) {
107
+ const nested = input.schools && typeof input.schools === 'object' ? input.schools : {};
108
+ const out = {};
109
+ for (const [key, allowed] of Object.entries(ZIWEI_SCHOOL_OPTIONS)) {
110
+ const raw = input[key] !== undefined ? input[key] : nested[key];
111
+ out[key] = allowed.includes(raw) ? raw : ZIWEI_SCHOOL_DEFAULTS[key];
112
+ }
113
+ return out;
114
+ }
115
+
116
+ // 亮度表按流派取值:数值 1–7 按各派档位换成中文。
117
+ // QS / XD1 为七档(1庙…7陷),ZZ / XD2 为五档(1庙 2旺 3平 4闲 5陷)。
118
+ const BRIGHTNESS_LEVELS_7 = ['', '庙', '旺', '得', '利', '平', '不', '陷'];
119
+ const BRIGHTNESS_LEVELS_5 = ['', '庙', '旺', '平', '闲', '陷'];
120
+ export const BRIGHTNESS_SCHOOLS = {
121
+ QS: {
122
+ levels: BRIGHTNESS_LEVELS_7,
123
+ rows: {
124
+ 紫微: [5, 1, 2, 2, 3, 2, 1, 1, 2, 2, 3, 2],
125
+ 天机: [1, 7, 3, 2, 4, 5, 1, 7, 3, 2, 4, 5],
126
+ 太阳: [7, 6, 2, 1, 2, 2, 2, 3, 3, 5, 6, 7],
127
+ 武曲: [2, 1, 3, 4, 1, 5, 2, 1, 3, 4, 1, 5],
128
+ 天同: [2, 6, 4, 5, 5, 1, 7, 6, 2, 5, 5, 1],
129
+ 廉贞: [5, 4, 1, 5, 4, 7, 5, 4, 1, 5, 4, 7],
130
+ 天府: [1, 1, 1, 3, 1, 3, 2, 1, 3, 2, 1, 3],
131
+ 太阴: [1, 1, 2, 7, 7, 7, 6, 6, 4, 2, 2, 1],
132
+ 贪狼: [2, 1, 5, 4, 1, 7, 2, 1, 5, 4, 1, 7],
133
+ 巨门: [2, 6, 1, 1, 7, 2, 2, 6, 1, 1, 7, 2],
134
+ 天相: [1, 1, 1, 7, 3, 3, 1, 3, 1, 7, 3, 3],
135
+ 天梁: [1, 2, 1, 1, 1, 3, 1, 2, 7, 3, 1, 7],
136
+ 七杀: [2, 1, 1, 2, 1, 5, 2, 1, 1, 2, 1, 5],
137
+ 破军: [1, 2, 3, 7, 2, 5, 1, 2, 3, 7, 2, 5],
138
+ },
139
+ },
140
+ XD1: {
141
+ levels: BRIGHTNESS_LEVELS_7,
142
+ rows: {
143
+ 紫微: [5, 1, 1, 2, 3, 2, 1, 1, 2, 2, 3, 2],
144
+ 天机: [1, 7, 3, 2, 4, 5, 1, 7, 3, 2, 4, 5],
145
+ 太阳: [7, 6, 2, 1, 2, 2, 1, 3, 4, 5, 7, 7],
146
+ 武曲: [2, 1, 3, 4, 1, 5, 2, 1, 3, 4, 1, 5],
147
+ 天同: [2, 6, 4, 1, 5, 1, 7, 6, 2, 5, 5, 1],
148
+ 廉贞: [5, 4, 1, 5, 4, 7, 5, 4, 1, 5, 4, 7],
149
+ 天府: [1, 1, 1, 3, 2, 3, 2, 1, 3, 2, 2, 3],
150
+ 太阴: [1, 1, 6, 7, 7, 7, 7, 5, 4, 2, 2, 1],
151
+ 贪狼: [2, 1, 5, 3, 1, 7, 2, 1, 5, 4, 1, 7],
152
+ 巨门: [2, 6, 1, 1, 5, 5, 2, 5, 1, 1, 5, 2],
153
+ 天相: [1, 1, 1, 7, 5, 3, 3, 3, 1, 7, 3, 3],
154
+ 天梁: [1, 2, 1, 2, 1, 7, 1, 2, 7, 3, 1, 7],
155
+ 七杀: [2, 1, 1, 2, 3, 5, 2, 1, 1, 2, 1, 5],
156
+ 破军: [1, 2, 3, 7, 2, 5, 1, 2, 3, 7, 2, 5],
157
+ },
158
+ },
159
+ ZZ: {
160
+ levels: BRIGHTNESS_LEVELS_5,
161
+ rows: {
162
+ 紫微: [3, 1, 1, 2, 5, 2, 1, 1, 2, 3, 4, 2],
163
+ 天机: [1, 5, 2, 2, 1, 3, 1, 5, 3, 2, 1, 3],
164
+ 太阳: [5, 5, 2, 1, 2, 2, 1, 3, 4, 4, 5, 5],
165
+ 武曲: [2, 1, 4, 5, 1, 3, 2, 1, 3, 2, 1, 3],
166
+ 天同: [2, 5, 4, 1, 3, 1, 5, 5, 2, 3, 3, 1],
167
+ 廉贞: [3, 2, 1, 4, 2, 5, 3, 1, 1, 3, 2, 5],
168
+ 天府: [1, 1, 1, 3, 1, 3, 2, 1, 3, 5, 1, 2],
169
+ 太阴: [1, 1, 4, 5, 4, 5, 5, 3, 3, 2, 2, 1],
170
+ 贪狼: [2, 1, 3, 4, 1, 5, 2, 1, 3, 3, 1, 5],
171
+ 巨门: [2, 2, 1, 1, 3, 3, 2, 5, 1, 1, 2, 2],
172
+ 天相: [1, 1, 1, 5, 2, 3, 2, 4, 1, 5, 4, 3],
173
+ 天梁: [1, 2, 1, 1, 2, 5, 1, 2, 5, 4, 2, 5],
174
+ 七杀: [2, 1, 1, 5, 2, 3, 2, 2, 1, 4, 1, 3],
175
+ 破军: [1, 2, 5, 2, 2, 4, 1, 1, 5, 5, 2, 3],
176
+ },
177
+ },
178
+ XD2: {
179
+ levels: BRIGHTNESS_LEVELS_5,
180
+ rows: {
181
+ 紫微: [4, 1, 1, 2, 4, 2, 1, 1, 2, 3, 5, 2],
182
+ 天机: [1, 5, 2, 2, 2, 3, 1, 5, 2, 2, 2, 3],
183
+ 太阳: [5, 5, 2, 1, 2, 2, 1, 2, 3, 3, 5, 5],
184
+ 武曲: [2, 1, 3, 5, 1, 3, 2, 1, 3, 2, 1, 3],
185
+ 天同: [2, 5, 3, 1, 2, 1, 5, 5, 2, 3, 3, 1],
186
+ 廉贞: [3, 2, 1, 4, 2, 5, 3, 1, 1, 3, 2, 5],
187
+ 天府: [2, 1, 1, 3, 1, 2, 2, 1, 2, 3, 1, 2],
188
+ 太阴: [1, 1, 5, 5, 5, 5, 5, 5, 2, 2, 1, 1],
189
+ 贪狼: [2, 1, 3, 2, 1, 5, 2, 1, 3, 2, 1, 5],
190
+ 巨门: [2, 5, 1, 2, 5, 1, 2, 5, 1, 2, 5, 1],
191
+ 天相: [1, 1, 1, 5, 2, 3, 2, 3, 1, 5, 5, 3],
192
+ 天梁: [1, 2, 1, 1, 2, 5, 1, 2, 5, 3, 2, 5],
193
+ 七杀: [2, 2, 1, 5, 1, 3, 2, 2, 1, 3, 1, 3],
194
+ 破军: [1, 1, 5, 2, 2, 3, 1, 1, 5, 5, 2, 3],
195
+ },
196
+ },
197
+ };
198
+
199
+ export function schoolBrightness(name, branchIndex, school = 'default') {
200
+ const table = BRIGHTNESS_SCHOOLS[school];
201
+ if (!table || !table.rows[name]) return BRIGHTNESS[name] ? BRIGHTNESS[name][fix(branchIndex)] : undefined;
202
+ const level = table.rows[name][fix(branchIndex)];
203
+ return level ? table.levels[level] : undefined;
204
+ }
205
+
206
+ // 庚干四化五变体
207
+ const GENG_MUTAGEN = {
208
+ GYWYT: { 太阳: '禄', 武曲: '权', 太阴: '科', 天同: '忌' },
209
+ GYWTY: { 太阳: '禄', 武曲: '权', 天同: '科', 太阴: '忌' },
210
+ GYWFT: { 太阳: '禄', 武曲: '权', 天府: '科', 天同: '忌' },
211
+ GYWFX: { 太阳: '禄', 武曲: '权', 天府: '科', 天相: '忌' },
212
+ GYWTX: { 太阳: '禄', 武曲: '权', 天同: '科', 天相: '忌' },
213
+ };
214
+
215
+ function mutagenOf(stem, schools) {
216
+ if (stem === '庚') return GENG_MUTAGEN[schools.gengYearSihua] || MUTAGEN.庚;
217
+ return MUTAGEN[stem] ?? {};
218
+ }
219
+
220
+ // 安魁钺四派:差异集中在庚辛壬癸年
221
+ export const KUI_YUE_SCHOOLS = {
222
+ qs2: {
223
+ 甲: [1, 7], 戊: [1, 7], 庚: [6, 2], 乙: [0, 8], 己: [0, 8],
224
+ 丙: [11, 9], 丁: [11, 9], 辛: [6, 2], 壬: [5, 3], 癸: [5, 3],
225
+ },
226
+ 'zhong-yiming': {
227
+ 甲: [1, 7], 戊: [1, 7], 庚: [2, 6], 乙: [0, 8], 己: [0, 8],
228
+ 丙: [11, 9], 丁: [11, 9], 辛: [2, 6], 壬: [5, 3], 癸: [5, 3],
229
+ },
230
+ network: {
231
+ 甲: [1, 7], 戊: [1, 7], 庚: [1, 7], 乙: [0, 8], 己: [0, 8],
232
+ 丙: [11, 9], 丁: [11, 9], 辛: [6, 2], 壬: [3, 5], 癸: [3, 5],
233
+ },
234
+ };
235
+
236
+ // 截空(按生年干):single 单星、double 正副双星、zhanyan 占验派
237
+ const JIEKONG = {
238
+ single: { 甲: 8, 乙: 6, 丙: 4, 丁: 2, 戊: 0, 己: 9, 庚: 7, 辛: 5, 壬: 3, 癸: 1 },
239
+ double: {
240
+ 甲: [8, 9], 乙: [7, 6], 丙: [4, 5], 丁: [3, 2], 戊: [0, 1],
241
+ 己: [9, 8], 庚: [6, 7], 辛: [5, 4], 壬: [2, 3], 癸: [1, 0],
242
+ },
243
+ zhanyan: { 甲: 8, 乙: 6, 丙: 4, 丁: 2, 戊: 10, 己: 9, 庚: 7, 辛: 5, 壬: 3, 癸: 11 },
244
+ };
245
+
77
246
  // 年干系
78
247
  export const LUCUN = { 甲: 2, 乙: 3, 丙: 5, 丁: 6, 戊: 5, 己: 6, 庚: 8, 辛: 9, 壬: 11, 癸: 0 };
79
248
  export const KUI_YUE = {
80
249
  甲: [1, 7], 戊: [1, 7], 庚: [1, 7], 乙: [0, 8], 己: [0, 8],
81
250
  丙: [11, 9], 丁: [11, 9], 辛: [6, 2], 壬: [3, 5], 癸: [3, 5],
82
251
  };
252
+ KUI_YUE_SCHOOLS.qs1 = KUI_YUE;
83
253
 
84
254
  /** 命主:按命宫地支 */
85
255
  export const MING_ZHU = {
@@ -192,12 +362,31 @@ export function xiaoxianAge(branchIndex, triad, gender) {
192
362
 
193
363
  // ── 主流程 ────────────────────────────────────────────────────
194
364
 
365
+ /**
366
+ * 闰月口径:split 按日切分(1–15 归本月、16 起归次月,主流排盘默认)、
367
+ * next 整月归次月、prev 整月归本月。`leapAsNext: true` 是 next 的旧写法。
368
+ */
369
+ export const LEAP_POLICIES = ['split', 'next', 'prev'];
370
+
371
+ export function leapPolicyOf(input) {
372
+ if (input && LEAP_POLICIES.includes(input.leapMonth)) return input.leapMonth;
373
+ return input && input.leapAsNext ? 'next' : 'split';
374
+ }
375
+
376
+ export function effectiveLunarMonth(raw, policy) {
377
+ if (!raw || !raw.isLeap) return raw ? raw.month : 0;
378
+ if (policy === 'next') return (raw.month % 12) + 1;
379
+ if (policy === 'split') return raw.day <= 15 ? raw.month : (raw.month % 12) + 1;
380
+ return raw.month;
381
+ }
382
+
195
383
  /**
196
384
  * 排紫微斗数命盘
197
- * @param {object} input { year,month,day,hour,minute, gender, leapAsNext? }
385
+ * @param {object} input { year,month,day,hour,minute, gender, leapMonth? }
198
386
  */
199
387
  export function buildZiwei(input) {
200
388
  assertCivil(input);
389
+ const schools = ziweiSchoolsOf(input);
201
390
  const { year: cy, month: cm, day: cd, hour: ch = 0, minute: cmin = 0, gender = 'male' } = input;
202
391
  const useTrueSolar = input.trueSolar !== false;
203
392
  const geo = input.geo || (input.address ? resolveAddress(input.address) : null);
@@ -212,7 +401,9 @@ export function buildZiwei(input) {
212
401
  ? applyTrueSolar(civilJd, longitude)
213
402
  : { offsetMinutes: 0, jd: civilJd, note: '未启用真太阳时' };
214
403
  // 紫微按「时辰」起盘:真太阳时若跨时辰/跨日,命盘随之改变
215
- const eff = jdToParts(solar.jd);
404
+ // 晚子时 next-day:23:00 后起盘日顺延一日,只改起盘日,不改真太阳时显示
405
+ let eff = jdToParts(solar.jd);
406
+ if (schools.lateZishi === 'next-day' && eff.hour >= 23) eff = jdToParts(solar.jd + 1);
216
407
  const year = eff.year;
217
408
  const month = eff.month;
218
409
  const day = eff.day;
@@ -220,9 +411,8 @@ export function buildZiwei(input) {
220
411
  const minute = eff.minute;
221
412
 
222
413
  const lunarRaw = solarToLunar(year, month, day);
223
- const lunarMonth = lunarRaw.isLeap && input.leapAsNext
224
- ? (lunarRaw.month % 12) + 1
225
- : lunarRaw.month;
414
+ const leapPolicy = leapPolicyOf(input);
415
+ const lunarMonth = effectiveLunarMonth(lunarRaw, leapPolicy);
226
416
  const lunarDay = lunarRaw.day;
227
417
 
228
418
  const hb = hourBranchIndex(hour, minute);
@@ -232,6 +422,10 @@ export function buildZiwei(input) {
232
422
  const yearStem = yearGanzhi.stem;
233
423
  const yb = BRANCHES.indexOf(yearGanzhi.branch);
234
424
  const triad = triadOf(yb);
425
+ // 大限顺逆:阳男阴女顺行、阴男阳女逆行(天伤天使的中州派互换也要用它)
426
+ const yearYang = STEMS.indexOf(yearStem) % 2 === 0;
427
+ const forward = (yearYang && gender === 'male') || (!yearYang && gender === 'female');
428
+ const kuiyueTable = KUI_YUE_SCHOOLS[schools.kuiyueSchool] || KUI_YUE;
235
429
 
236
430
  // 命宫 / 身宫
237
431
  const soulIndex = fix(2 + (lunarMonth - 1) - hb);
@@ -247,12 +441,12 @@ export function buildZiwei(input) {
247
441
 
248
442
  // 星曜落宫
249
443
  const starsByBranch = Array.from({ length: 12 }, () => []);
250
- // 安星:主星顺带记下所在宫的庙旺得利平不陷(BRIGHTNESS 以地支序索引,与 fix(branch) 一致)
444
+ // 安星:主星顺带记下所在宫的庙旺得利平不陷(亮度可按流派切换,默认用内置通行表)
251
445
  const put = (branch, name, type) => {
252
446
  const idx = fix(branch);
253
447
  const star = { name, type };
254
448
  if (type === '主星' && BRIGHTNESS[name]) {
255
- const b = BRIGHTNESS[name][idx];
449
+ const b = schoolBrightness(name, idx, schools.brightnessSchool);
256
450
  if (b) star.brightness = b;
257
451
  }
258
452
  starsByBranch[idx].push(star);
@@ -278,7 +472,7 @@ export function buildZiwei(input) {
278
472
  put(lucun, '禄存', '吉星');
279
473
  put(lucun + 1, '擎羊', '煞星');
280
474
  put(lucun - 1, '陀罗', '煞星');
281
- const [kui, yue] = KUI_YUE[yearStem];
475
+ const [kui, yue] = kuiyueTable[yearStem];
282
476
  put(kui, '天魁', '吉星');
283
477
  put(yue, '天钺', '吉星');
284
478
 
@@ -290,7 +484,11 @@ export function buildZiwei(input) {
290
484
  put(6 + yb, '天虚', '煞星');
291
485
  put(4 + yb, '龙池', '吉星');
292
486
  put(10 - yb, '凤阁', '吉星');
293
- put(TIANMA[triad], '天马', '吉星');
487
+ // 天马:默认按年支三合,可切生月三合(正月起寅)
488
+ const maTriad = schools.tianmaSchool === 'month-branch'
489
+ ? triadOf(fix(2 + (lunarMonth - 1)))
490
+ : triad;
491
+ put(TIANMA[maTriad], '天马', '吉星');
294
492
  put(XIANCHI[triad], '咸池', '煞星');
295
493
  put(HUAGAI[triad], '华盖', '杂曜');
296
494
  const dirGroup = directionOf(yb);
@@ -298,6 +496,25 @@ export function buildZiwei(input) {
298
496
  put(GUASU[dirGroup], '寡宿', '煞星');
299
497
  put(posuIndex(yb), '破碎', '煞星');
300
498
 
499
+ // 空曜(默认不排,旧盘保持 49 颗)
500
+ if (schools.tiankongSchool !== 'off') {
501
+ put(schools.tiankongSchool === 'shun-jia-shi' ? yb + hb : yb + 1, '天空', '杂曜');
502
+ }
503
+ if (schools.jiekongSchool !== 'off') {
504
+ const jk = JIEKONG[schools.jiekongSchool][yearStem];
505
+ if (Array.isArray(jk)) {
506
+ put(jk[0], '截空', '杂曜');
507
+ put(jk[1], '副截', '杂曜');
508
+ } else {
509
+ put(jk, '截空', '杂曜');
510
+ }
511
+ // 旬空:本旬十日所空两支(年干支所在旬的末两位)
512
+ const nianIndex = (((lunarRaw.lunarYear - 4) % 60) + 60) % 60;
513
+ const decadeStart = nianIndex - (nianIndex % 10);
514
+ put(decadeStart + 10, '旬空', '杂曜');
515
+ put(decadeStart + 11, '副旬', '杂曜');
516
+ }
517
+
301
518
  // 月系
302
519
  put(9 + (lunarMonth - 1), '天刑', '煞星'); // 酉起正月顺行
303
520
  put(1 + (lunarMonth - 1), '天姚', '煞星'); // 丑起正月顺行
@@ -314,12 +531,13 @@ export function buildZiwei(input) {
314
531
  put(11 + hb, '地劫', '煞星'); // 亥宫起子时,顺行
315
532
  put(11 - hb, '地空', '煞星'); // 亥宫起子时,逆行
316
533
 
317
- // 固定宫位:天伤恒在交友,天使恒在疾厄
318
- put(soulIndex - 7, '天伤', '杂曜');
319
- put(soulIndex - 5, '天使', '杂曜');
534
+ // 固定宫位:天伤恒在交友、天使恒在疾厄;中州派阴男阳女互换
535
+ const swapTianshi = schools.tianshiTianshang === 'zhongzhou' && !forward;
536
+ put(soulIndex - (swapTianshi ? 5 : 7), '天伤', '杂曜');
537
+ put(soulIndex - (swapTianshi ? 7 : 5), '天使', '杂曜');
320
538
 
321
- // 生年四化
322
- const mutagenMap = MUTAGEN[yearStem] ?? {};
539
+ // 生年四化(庚干按所选变体)
540
+ const mutagenMap = mutagenOf(yearStem, schools);
323
541
  for (const list of starsByBranch) {
324
542
  for (const star of list) {
325
543
  if (mutagenMap[star.name]) star.mutagen = mutagenMap[star.name];
@@ -330,13 +548,14 @@ export function buildZiwei(input) {
330
548
  const palaceNames = [];
331
549
  for (let i = 0; i < 12; i++) palaceNames[fix(soulIndex - i)] = PALACE_NAMES[i];
332
550
 
333
- // 大限:阳男阴女顺行、阴男阳女逆行
334
- const yearYang = STEMS.indexOf(yearStem) % 2 === 0;
335
- const forward = (yearYang && gender === 'male') || (!yearYang && gender === 'female');
551
+ // 大限:阳男阴女顺行、阴男阳女逆行(forward 已在安星前算好)
336
552
  const startAge = bureau.number;
337
553
 
338
554
  // 十二神(长生/博士 随大限顺逆,将前/岁前 恒顺行)
339
- const csStart = CHANGSHENG_START[bureau.number];
555
+ // 土五局长生起点按流派:默认申,河洛/火土派取寅
556
+ const csStart = schools.changshengSchool === 'huo-tu' && bureau.number === 5
557
+ ? 2
558
+ : CHANGSHENG_START[bureau.number];
340
559
  const shenshaAt = (branch) => {
341
560
  const d = forward ? 1 : -1;
342
561
  return {
@@ -442,8 +661,8 @@ export function buildZiwei(input) {
442
661
  liulu: BRANCHES[lu],
443
662
  liuyang: BRANCHES[fix(lu + 1)],
444
663
  liutuo: BRANCHES[fix(lu - 1)],
445
- liukui: BRANCHES[KUI_YUE[gz.stem][0]],
446
- liuyue: BRANCHES[KUI_YUE[gz.stem][1]],
664
+ liukui: BRANCHES[kuiyueTable[gz.stem][0]],
665
+ liuyue: BRANCHES[kuiyueTable[gz.stem][1]],
447
666
  mutagen: Object.entries(MUTAGEN[gz.stem] ?? {}).map(([s, k]) => `${s}化${k}`),
448
667
  daxian: dxPalace
449
668
  ? { palace: dxPalace.palace, start: dxPalace.daxian.start, end: dxPalace.daxian.end }
@@ -457,13 +676,16 @@ export function buildZiwei(input) {
457
676
  input: {
458
677
  year: cy, month: cm, day: cd, hour: ch, minute: cmin,
459
678
  gender,
460
- leapAsNext: Boolean(input.leapAsNext),
679
+ leapMonth: leapPolicy,
680
+ leapAsNext: leapPolicy === 'next',
461
681
  longitude: longitude ?? null,
462
682
  address: input.address || null,
463
683
  geo,
464
684
  trueSolar: useTrueSolar,
465
685
  dst: { applied: !!dst.applied, note: dst.note || null },
686
+ schools: { ...schools },
466
687
  },
688
+ schools: { ...schools },
467
689
  solarTime: {
468
690
  civilText: `${cy}-${String(cm).padStart(2, '0')}-${String(cd).padStart(2, '0')} ${String(ch).padStart(2, '0')}:${String(cmin).padStart(2, '0')}`,
469
691
  trueSolarText: formatJd(solar.jd),
@@ -473,10 +695,16 @@ export function buildZiwei(input) {
473
695
  lunar: {
474
696
  year: lunarRaw.lunarYear,
475
697
  month: lunarMonth,
698
+ monthRaw: lunarRaw.month,
476
699
  day: lunarDay,
477
700
  isLeap: lunarRaw.isLeap,
701
+ leapPolicy,
478
702
  monthStart: lunarRaw.monthStart,
479
- ...(lunarRaw.isLeap ? { note: '闰月按本月起盘(可用 leapAsNext 改为按次月)' } : {}),
703
+ ...(lunarRaw.isLeap ? {
704
+ note: leapPolicy === 'split'
705
+ ? `闰${lunarRaw.month}月按日切分:1–15 按本月、16 起按次月;本盘闰月${lunarDay}日按${lunarMonth}月`
706
+ : leapPolicy === 'next' ? '闰月整月按次月起盘' : '闰月整月按本月起盘',
707
+ } : {}),
480
708
  },
481
709
  hourBranch,
482
710
  yearGanzhi: yearGanzhi.name,
@@ -512,12 +740,14 @@ export function buildZiwei(input) {
512
740
  },
513
741
  rules: {
514
742
  ziweiPlacement: '借数法:局数除日数取商,商数自寅宫顺行,借数奇逆偶顺',
515
- mutagen: '生年四化用中州派常用表;庚干各派有异(有以天同化科、太阴化忌者)',
516
- leapMonth: '闰月默认按本月,可切换为按次月',
743
+ mutagen: '生年四化用中州派常用表;庚干可按流派切换(默认阳武阴同,另有同科阴忌、府科、府科相忌、同科相忌)',
744
+ schools: `流派开关:亮度 ${schools.brightnessSchool}、庚干四化 ${schools.gengYearSihua}、天马 ${schools.tianmaSchool}、天空 ${schools.tiankongSchool}、截空 ${schools.jiekongSchool}、魁钺 ${schools.kuiyueSchool}、天伤天使 ${schools.tianshiTianshang}、长生 ${schools.changshengSchool}、晚子时 ${schools.lateZishi};庚干四化只作用于生年四化`,
745
+ leapMonth: '闰月默认按日切分(1–15 归本月、16 起归次月,主流排盘口径);可传 leapMonth: next(整月归次月)或 prev(整月归本月),leapAsNext: true 等同 next',
517
746
  auxStars: '火铃按年支三合起点顺数至生时;羊陀夹禄存;魁钺按年干;地空亥起子时逆行、地劫顺行;三台八座随辅弼;恩光天贵随昌曲退一位;台辅封诰按时支',
518
747
  shensha: '长生、博士随大限顺逆;将前、岁前恒顺行;天伤恒在交友、天使恒在疾厄',
519
748
  masters: '命主按命宫地支,身主按生年地支',
520
749
  liunian: '流年命宫取该年地支宫;三方四正为本宫与 +4/+8/+6;并给该年所属大限与小限',
750
+ flyComposite: '三方四正取本宫、对宫与两个三合宫共四宫,四宫宫干各化禄权科忌共 16 条,按落宫归堆标会聚(双忌会、禄忌同宫、三化会、自化);只描会照,不折吉凶',
521
751
  changqu: '昌曲落宫只由时支决定,故与丑未的关系(夹/三合/同宫/无关)是结构性结论;absorb 的「清/杂」是启发式标记,依据昌曲化忌与会煞,不是断语',
522
752
  },
523
753
  };
@@ -526,14 +756,16 @@ export function buildZiwei(input) {
526
756
  chart.daxianList = daxianList(chart);
527
757
  chart.fly = flyStars(chart);
528
758
  chart.flyChains = flyChains(chart, { from: '命宫' });
759
+ chart.flyComposite = flyComposite(chart, { from: '命宫' });
529
760
  enrichLiunian(chart);
530
761
  // 流月/流日/流时:默认按本机当前时刻,可用 asOf 指定(如 "2026-10-22T16:00")
531
762
  chart.flow = flowOf(chart, input.asOf);
532
763
  chart.patternSummary = {
533
764
  count: chart.patterns.length,
534
765
  good: chart.patterns.filter((p) => p.tone === 'good').map((p) => p.name),
766
+ neutral: chart.patterns.filter((p) => p.tone === 'neutral').map((p) => p.name),
535
767
  bad: chart.patterns.filter((p) => p.tone === 'bad').map((p) => p.name),
536
- note: '格局只描述结构,不作吉凶断语;同一命盘可并存吉格与忌格',
768
+ note: '格局只描述结构,不作吉凶断语;同一命盘可并存吉格、中性结构与忌格',
537
769
  };
538
770
  return chart;
539
771
  }
@@ -575,19 +807,18 @@ export function detectPatterns(chart) {
575
807
 
576
808
  const lu = starBranchOf(chart, '禄存');
577
809
  const ma = starBranchOf(chart, '天马');
578
- if (lu >= 0 && ma >= 0 && (lu === ma || fix(lu - ma) === 6)) add('禄马交驰', `禄存在${chart.ziwei && BRANCHES[lu]},天马在${BRANCHES[ma]}`);
810
+ if (lu >= 0 && ma >= 0 && (lu === ma || fix(lu - ma) === 6)) add('禄马交驰', `禄存在${BRANCHES[lu]},天马在${BRANCHES[ma]}`);
579
811
 
580
812
  const huo = starBranchOf(chart, '火星');
581
813
  const ling = starBranchOf(chart, '铃星');
582
814
  if (fix(soulIdx - 1) === huo && fix(soulIdx + 1) === ling) add('火铃夹命', '火星在命宫前一宫、铃星在后一宫夹命');
583
815
  if (fix(soulIdx - 1) === ling && fix(soulIdx + 1) === huo) add('火铃夹命', '铃星与火星夹命');
584
816
 
817
+ // 擎羊在禄存 +1、陀罗在禄存 -1,两星只能夹「禄存」那一宫;
818
+ // 所以「羊陀夹忌」实际就是化忌与禄存同宫,按左右两侧去找永远找不到。
585
819
  const ji = chart.palaces.find((p) => p.stars.some((s) => s.mutagen === '忌'));
586
- if (ji) {
587
- const jb = BRANCHES.indexOf(ji.branch);
588
- if (fix(jb - 1) === starBranchOf(chart, '擎羊') && fix(jb + 1) === starBranchOf(chart, '陀罗')) {
589
- add('羊陀夹忌', `化忌在${ji.branch},擎羊陀罗夹之`);
590
- }
820
+ if (ji && lu >= 0 && BRANCHES.indexOf(ji.branch) === lu) {
821
+ add('羊陀夹忌', `化忌与禄存在${ji.branch}同宫,擎羊陀罗夹之`);
591
822
  }
592
823
 
593
824
  // 铃昌陀武:四星会于命宫三方四正
@@ -605,10 +836,14 @@ export function detectPatterns(chart) {
605
836
  // ── 以下为扩展格局(条件都是星位判断,evidence 写明落在哪宫)──
606
837
  const inSoul = (name) => soul.stars.some((s) => s.name === name);
607
838
  const nearStar = (name) => near.has(name);
839
+ // 「夹」只要求两星分居命宫前后,不分左右——日月、左右、魁钺本来就没有固定左右;
840
+ // 旧写法只认一种顺序,昌曲夹命与羊陀夹命因此从未命中,日月/左右/魁钺也漏掉一半。
608
841
  const jia = (a, b) => {
609
842
  const ba = starBranchOf(chart, a);
610
843
  const bb = starBranchOf(chart, b);
611
- return ba >= 0 && bb >= 0 && fix(ba + 1) === soulIdx && fix(bb - 1) === soulIdx;
844
+ if (ba < 0 || bb < 0) return false;
845
+ return (fix(ba + 1) === soulIdx && fix(bb - 1) === soulIdx)
846
+ || (fix(bb + 1) === soulIdx && fix(ba - 1) === soulIdx);
612
847
  };
613
848
  const mutagensIn = (indices) => {
614
849
  const set = new Set();
@@ -642,7 +877,7 @@ export function detectPatterns(chart) {
642
877
  if (jia('左辅', '右弼')) add('左右夹命', '左辅、右弼夹命宫');
643
878
  if (jia('文昌', '文曲')) add('昌曲夹命', '文昌、文曲夹命宫');
644
879
  if (jia('天魁', '天钺')) add('魁钺夹命', '天魁、天钺夹命宫');
645
- if (jia('擎羊', '陀罗')) add('羊陀夹命', '擎羊、陀罗夹命宫');
880
+ if (jia('擎羊', '陀罗')) add('羊陀夹命', `禄存在${BRANCHES[soulIdx]}守命,擎羊陀罗前后夹之`);
646
881
 
647
882
  // 四化
648
883
  const muts = mutagensIn(sanfang);
@@ -657,9 +892,13 @@ export function detectPatterns(chart) {
657
892
  const lianzhen = starBranchOf(chart, '廉贞');
658
893
  const tianxiang = starBranchOf(chart, '天相');
659
894
  const qingyang = starBranchOf(chart, '擎羊');
660
- if (lianzhen >= 0 && qingyang >= 0 && tianxiang >= 0
661
- && fix(lianzhen + 1) === tianxiang && fix(qingyang - 1) === tianxiang) {
662
- add('刑囚夹印', '廉贞与擎羊夹天相(刑囚夹印)');
895
+ // 天相与廉贞永远隔偶数宫,字面上的「廉贞、擎羊夹天相」不可能成立;
896
+ // 通行说法取「廉贞天相同守命宫于子午、擎羊同宫」——判的是守命那一宫,其它宫位同宫不算。
897
+ // 命宫卯酉、廉贞天相对照遇擎羊的变体未纳入。
898
+ if (lianzhen >= 0 && tianxiang >= 0 && qingyang >= 0
899
+ && lianzhen === tianxiang && qingyang === tianxiang && tianxiang === soulIdx
900
+ && (tianxiang === 0 || tianxiang === 6)) {
901
+ add('刑囚夹印', `廉贞天相同守命宫${BRANCHES[tianxiang]},擎羊同宫`);
663
902
  }
664
903
  if (near.has('贪狼') && (near.has('火星') || near.has('铃星'))) {
665
904
  add('火贪铃贪', `贪狼与${near.has('火星') ? '火星' : '铃星'}会于命宫三方四正`);
@@ -676,7 +915,7 @@ export function detectPatterns(chart) {
676
915
  .map((p) => ({ ...p, tone: PATTERN_TONE[p.name] || 'neutral' }));
677
916
  }
678
917
 
679
- const PATTERN_TONE = {
918
+ export const PATTERN_TONE = {
680
919
  紫府同宫: 'good', 紫府朝垣: 'good', 君臣庆会: 'good', 极向离明: 'good',
681
920
  七杀朝斗: 'good', 英星入庙: 'good', 石中隐玉: 'good', 巨日同宫: 'good',
682
921
  日月夹命: 'good', 日月对照: 'good', 左右夹命: 'good', 昌曲夹命: 'good',
@@ -795,6 +1034,102 @@ export function flyChains(chart, { from = '命宫', maxSteps = 12 } = {}) {
795
1034
  return out;
796
1035
  }
797
1036
 
1037
+ // ── 三方四正复合飞化:四宫一起飞、看落到同一宫的会聚 ─────────────────
1038
+ // 单宫飞化回答「这一宫自己去找谁」;三方四正看的是本宫连同对宫、两个三合宫
1039
+ // 四股力同时飞出去,最后在哪一宫会到一起。会在一处的化越多,那一宫被牵动的力道越集中。
1040
+ // 只归堆与标会照,不折吉凶:同样是双忌会一宫,落在生年忌和落在生年禄上意思完全不同。
1041
+ const PALACE_SUFFIX = (p) => `${p}${String(p).endsWith('宫') ? '' : '宫'}`;
1042
+
1043
+ export function flyComposite(chart, { from = '命宫' } = {}) {
1044
+ const palaces = chart.palaces || [];
1045
+ const src = palaces.find((p) => p.palace === from) || palaces[0];
1046
+ if (!src) return null;
1047
+ const names = Array.isArray(src.sanfang) && src.sanfang.length
1048
+ ? [...new Set(src.sanfang)].slice(0, 4)
1049
+ : [src.palace];
1050
+
1051
+ const sources = [];
1052
+ for (const name of names) {
1053
+ const p = palaces.find((x) => x.palace === name);
1054
+ if (!p) continue;
1055
+ sources.push({
1056
+ palace: p.palace,
1057
+ branch: p.branch,
1058
+ stem: p.stem,
1059
+ flies: FLY_TYPES.map((type) => {
1060
+ const star = flyStarOf(p.stem, type);
1061
+ const b = star ? starBranchOf(chart, star) : -1;
1062
+ const target = b >= 0 ? palaces.find((x) => x.branch === BRANCHES[b]) : null;
1063
+ return {
1064
+ type,
1065
+ star,
1066
+ to: target ? target.palace : null,
1067
+ toBranch: target ? target.branch : null,
1068
+ self: !!(target && target.palace === p.palace),
1069
+ };
1070
+ }),
1071
+ });
1072
+ }
1073
+
1074
+ // 16 条飞化按落宫归堆;未上盘的星不计入落点
1075
+ const targets = [];
1076
+ for (const p of palaces) {
1077
+ const incoming = [];
1078
+ for (const s of sources) {
1079
+ for (const f of s.flies) {
1080
+ if (f.to === p.palace) {
1081
+ incoming.push({ from: s.palace, fromBranch: s.branch, fromStem: s.stem, type: f.type, star: f.star, self: f.self });
1082
+ }
1083
+ }
1084
+ }
1085
+ if (!incoming.length) continue;
1086
+ targets.push({
1087
+ palace: p.palace,
1088
+ branch: p.branch,
1089
+ incoming,
1090
+ types: [...new Set(incoming.map((x) => x.type))],
1091
+ count: incoming.length,
1092
+ });
1093
+ }
1094
+ targets.sort((a, b) => b.count - a.count || BRANCHES.indexOf(a.branch) - BRANCHES.indexOf(b.branch));
1095
+
1096
+ const jiAt = (t) => t.incoming.filter((x) => x.type === '忌').length;
1097
+ const groups = {
1098
+ doubleJi: targets.filter((t) => jiAt(t) >= 2),
1099
+ luJi: targets.filter((t) => t.types.includes('禄') && t.types.includes('忌')),
1100
+ tripleHua: targets.filter((t) => t.types.length >= 3),
1101
+ selfHua: targets.filter((t) => t.incoming.some((x) => x.self)),
1102
+ };
1103
+
1104
+ const labels = [];
1105
+ for (const t of groups.doubleJi) labels.push(`${['', '', '双忌', '三忌', '四忌'][jiAt(t)] || `${jiAt(t)}忌`}会${PALACE_SUFFIX(t.palace)}`);
1106
+ for (const t of groups.luJi) labels.push(`禄忌同宫${PALACE_SUFFIX(t.palace)}`);
1107
+ const HUA_NUM = { 3: '三化会', 4: '四化会' };
1108
+ for (const t of groups.tripleHua) labels.push(`${HUA_NUM[t.types.length] || `${t.types.length}化会`}${PALACE_SUFFIX(t.palace)}`);
1109
+ for (const t of groups.selfHua) labels.push(`${PALACE_SUFFIX(t.palace)}自化`);
1110
+
1111
+ const byType = {};
1112
+ for (const type of FLY_TYPES) {
1113
+ byType[type] = sources
1114
+ .map((s) => {
1115
+ const f = s.flies.find((x) => x.type === type);
1116
+ return f && f.to ? { from: s.palace, star: f.star, to: f.to, self: f.self } : null;
1117
+ })
1118
+ .filter(Boolean);
1119
+ }
1120
+
1121
+ return {
1122
+ from: src.palace,
1123
+ sanfang: sources.map((s) => s.palace),
1124
+ sources,
1125
+ targets,
1126
+ groups,
1127
+ byType,
1128
+ labels,
1129
+ note: '三方四正取本宫、对宫与两个三合宫共四宫,四宫宫干各化禄权科忌共 16 条飞化,按落宫归堆;只在落宫相同(会照)时标会聚,单条飞出不另立标签。只描会照与方向,不折吉凶,也未叠大限流年四化。',
1130
+ };
1131
+ }
1132
+
798
1133
  // ── 流年四化落宫 ────────────────────────────────────────────
799
1134
 
800
1135
  export function enrichLiunian(chart) {
@@ -844,8 +1179,7 @@ export function flowOf(chart, asOf) {
844
1179
  const MI = a.minute === undefined ? now.getMinutes() : a.minute;
845
1180
 
846
1181
  const lr = solarToLunar(Y, M, D);
847
- const leapAsNext = Boolean(chart.input && chart.input.leapAsNext);
848
- const natalMonth = chart.lunar.isLeap && leapAsNext ? (chart.lunar.month % 12) + 1 : chart.lunar.month;
1182
+ const natalMonth = chart.lunar.month; // 已在起盘时按闰月口径换算过,这里不再二次换月
849
1183
  const natalHB = BRANCHES.indexOf(chart.hourBranch);
850
1184
  const hbNow = hourBranchIndex(H, MI);
851
1185