dsh-metaphysics 0.13.5 → 0.14.2

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/lunar.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // 农历(定朔):朔望月计算 + 冬至所在月为十一月 + 无中气置闰
2
2
  import {
3
- toJDN, jdToParts, termInstantJd, deltaTSeconds,
3
+ toJDN, jdToParts, termInstantJd, deltaTSeconds, localOffsetFor, OFFSET_MODERN,
4
4
  } from './calendar.js';
5
5
 
6
6
  const SYNODIC = 29.530588861;
@@ -53,8 +53,9 @@ export function newMoonJd(k) {
53
53
  - 0.00002 * Math.sin(3 * Mp + M)
54
54
  + 0.00002 * Math.sin(4 * Mp);
55
55
 
56
- // TT -> UT -> 北京时间
57
- return jde + corr - deltaTSeconds(2000 + T * 100) / 86400 + 8 / 24;
56
+ // TT -> UT -> 当时本地时间轴(1929 前北京地方平时,之后东八区;与节气共用同一套换算)
57
+ const jdUT = jde + corr - deltaTSeconds(2000 + T * 100) / 86400;
58
+ return jdUT + localOffsetFor(jdUT);
58
59
  }
59
60
 
60
61
  /** 某时刻所在的「日」:返回当日 0 时的 JDN */
@@ -62,6 +63,13 @@ export function dayNumberOf(jd) {
62
63
  return Math.floor(jd + 0.5);
63
64
  }
64
65
 
66
+ // 1929 年前后的时区切换常量与换算统一放在 calendar.js(节气与农历共用)
67
+ /** 把 calendar 给出的「东八区轴」时刻换成当时的地方轴 */
68
+ function toHistoricalLocal(jdBeijing) {
69
+ const jdUt = jdBeijing - OFFSET_MODERN;
70
+ return jdUt + localOffsetFor(jdUt);
71
+ }
72
+
65
73
  function kNear(jdDay) {
66
74
  return Math.round((jdDay + 0.5 - NEW_MOON_EPOCH) / SYNODIC);
67
75
  }
@@ -70,8 +78,9 @@ function kNear(jdDay) {
70
78
  function hasZhongQi(year, startDay, endDay) {
71
79
  for (let y = year - 1; y <= year + 1; y++) {
72
80
  for (let i = 1; i < 24; i += 2) {
73
- const d = dayNumberOf(termInstantJd(y, i));
74
- if (d >= startDay && d < endDay) return { index: i, jd: termInstantJd(y, i) };
81
+ const jdLocal = toHistoricalLocal(termInstantJd(y, i));
82
+ const d = dayNumberOf(jdLocal);
83
+ if (d >= startDay && d < endDay) return { index: i, jd: jdLocal };
75
84
  }
76
85
  }
77
86
  return null;
@@ -84,24 +93,41 @@ function hasZhongQi(year, startDay, endDay) {
84
93
  export function solarToLunar(year, month, day) {
85
94
  const targetDay = toJDN(year, month, day);
86
95
 
87
- const wsDayThisYear = dayNumberOf(termInstantJd(year, 23));
96
+ const wsDayThisYear = dayNumberOf(toHistoricalLocal(termInstantJd(year, 23)));
88
97
  const wsYear = targetDay < wsDayThisYear ? year - 1 : year;
89
- const wsDay = dayNumberOf(termInstantJd(wsYear, 23));
98
+ const wsDay = dayNumberOf(toHistoricalLocal(termInstantJd(wsYear, 23)));
90
99
 
91
100
  // 十一月朔:日序不晚于冬至的最后一个朔日
92
101
  let k = kNear(wsDay);
93
102
  while (dayNumberOf(newMoonJd(k)) > wsDay) k--;
94
103
  while (dayNumberOf(newMoonJd(k + 1)) <= wsDay) k++;
95
104
 
105
+ // ── 置闰前提(国标口径)──
106
+ // 只有「两个冬至月之间有 13 个朔望月」才需要置闰,且取其中第一个无中气的月。
107
+ // 若只有 12 个月,即使某月无中气也不闰(1985 正月、2053 正月就是这种反例)。
108
+ const nextWsDay = dayNumberOf(toHistoricalLocal(termInstantJd(wsYear + 1, 23)));
109
+ let kNext = k;
110
+ while (dayNumberOf(newMoonJd(kNext + 1)) <= nextWsDay) kNext++;
111
+ const cycleMonths = kNext - k;
112
+ let leapIdx = -1;
113
+ if (cycleMonths === 13) {
114
+ for (let i = 1; i <= cycleMonths; i++) {
115
+ const s = dayNumberOf(newMoonJd(k + i - 1));
116
+ const e = dayNumberOf(newMoonJd(k + i));
117
+ if (!hasZhongQi(wsYear, s, e)) { leapIdx = i; break; }
118
+ }
119
+ }
120
+
96
121
  let num = 11;
97
- let leapUsed = false;
122
+ // 农历年以正月初一为界:月序越过「正」之后,岁次进一位
123
+ let wrapped = false;
98
124
  let cursorStart = dayNumberOf(newMoonJd(k));
99
125
 
100
126
  for (let i = 1; i <= 15; i++) {
101
127
  const startDay = dayNumberOf(newMoonJd(k + i - 1));
102
128
  const endDay = dayNumberOf(newMoonJd(k + i));
103
129
  const zq = hasZhongQi(wsYear, startDay, endDay);
104
- const isLeap = !zq && !leapUsed && i > 1;
130
+ const isLeap = i === leapIdx;
105
131
 
106
132
  if (targetDay >= startDay && targetDay < endDay) {
107
133
  const sp = jdToParts(newMoonJd(k + i - 1));
@@ -110,7 +136,7 @@ export function solarToLunar(year, month, day) {
110
136
  return {
111
137
  year: wsYear,
112
138
  // 农历年号:十一、十二月属上一农历年,正月至十月属 wsYear + 1
113
- lunarYear: displayNum >= 11 ? wsYear : wsYear + 1,
139
+ lunarYear: wrapped ? wsYear + 1 : wsYear,
114
140
  month: displayNum,
115
141
  day: targetDay - startDay + 1,
116
142
  isLeap,
@@ -119,8 +145,11 @@ export function solarToLunar(year, month, day) {
119
145
  };
120
146
  }
121
147
 
122
- if (isLeap) leapUsed = true;
123
- else num = (num % 12) + 1;
148
+ if (isLeap) { /* 闰月不推进月号 */ }
149
+ else {
150
+ num = (num % 12) + 1;
151
+ if (num === 1) wrapped = true; // 已越过正月初一
152
+ }
124
153
  cursorStart = endDay;
125
154
  }
126
155
 
@@ -133,3 +162,8 @@ export function monthStartJd(year, month, day) {
133
162
  }
134
163
 
135
164
  export { SYNODIC };
165
+
166
+
167
+
168
+
169
+
package/engine/meihua.js CHANGED
@@ -91,9 +91,9 @@ export function buildMeihua(input = {}) {
91
91
  const useTri = movingInUpper ? ben.upper : ben.lower;
92
92
  const rel = relation(TRI_ELEMENT[bodyTri], TRI_ELEMENT[useTri]);
93
93
 
94
- // 错卦(六爻全变)、综卦(上下颠倒)
94
+ // 错卦=六爻全变;综卦=整卦倒转(覆卦),不是上下卦对调:天风姤 → 泽天夬
95
95
  const cuoBits = bits.map((b) => (b ? 0 : 1));
96
- const zongBits = [...bits.slice(3), ...bits.slice(0, 3)];
96
+ const zongBits = bits.slice().reverse();
97
97
  const cuo = hexagramFromBits(cuoBits);
98
98
  const zong = hexagramFromBits(zongBits);
99
99
 
@@ -0,0 +1,176 @@
1
+ // 奇门断语:分层输出「可复算事实 → 传统解释 → 关系 → 条件式倾向」。
2
+ // 依据:「宫制其门不为迫,门制其宫门迫凶」——门迫严格限定同一宫内门五行克宫五行,
3
+ // 宫克门是「宫制门(制)」,不是门迫。措辞一律条件式,禁止「必成/必败/绝对」。
4
+ import { wuxingRelation, gatePalaceRelation } from './qimen.js';
5
+
6
+ const GATE_MEAN = {
7
+ 休门: '休为休息、蓄势,传统上主养精与谈判',
8
+ 生门: '生为生发、财源,传统上主求财与谋事',
9
+ 伤门: '伤为耗损、冲突,传统上主竞争与损耗',
10
+ 杜门: '杜为闭塞、隐藏,传统上主保密与研究',
11
+ 景门: '景为文书、声名,传统上主宣传与考试',
12
+ 死门: '死为终结、收束,传统上主了断与送旧',
13
+ 惊门: '惊为惊动、口舌,传统上主争辩与追讨',
14
+ 开门: '开为开张、通达,传统上主上任与开拓',
15
+ };
16
+ const STAR_MEAN = {
17
+ 天蓬: '天蓬主暗昧、胆大', 天任: '天任主厚稳、经营', 天冲: '天冲主快速、行动',
18
+ 天辅: '天辅主文教、辅助', 天英: '天英主显扬、热烈', 天芮: '天芮主病患、纠缠',
19
+ 天柱: '天柱主口舌、阻隔', 天心: '天心主谋略、医术', 天禽: '天禽主中正、协调', 禽芮: '禽芮主中正兼病气',
20
+ };
21
+ const GOD_MEAN = {
22
+ 值符: '值符为首领,主主导权与贵人', 腾蛇: '腾蛇主虚惊、反复', 太阴: '太阴主暗助、私密',
23
+ 六合: '六合主和合、媒介', 白虎: '白虎主刚烈、伤病', 玄武: '玄武主暗昧、失脱',
24
+ 九地: '九地主沉稳、守成', 九天: '九天主高远、扬动',
25
+ };
26
+ const YONG_SHEN = {
27
+ general: { label: '综合', primary: ['开门'], secondary: ['生门'], note: '综合以开、生两门为候选用神' },
28
+ wealth: { label: '财运', primary: ['生门'], secondary: ['开门'], note: '求财以生门为主,开门为辅' },
29
+ career: { label: '事业', primary: ['开门'], secondary: ['值符'], note: '事业以开门为主,值符(主导权)为辅' },
30
+ love: { label: '感情', primary: ['休门'], secondary: ['六合'], note: '感情以休门为主,六合(和合)为辅' },
31
+ study: { label: '学业', primary: ['景门'], secondary: ['天辅'], note: '学业以景门为主,天辅(文教)为辅' },
32
+ health: { label: '健康', primary: ['天芮'], secondary: ['死门'], note: '问病以天芮为主,死门为辅;只作传统取象,不作诊断' },
33
+ travel: { label: '出行', primary: ['九天'], secondary: ['驿马'], note: '出行以九天为主,兼看驿马' },
34
+ };
35
+
36
+ const REL_TEXT = {
37
+ 生: '主体力量流向环境或趋势',
38
+ 克: '主体对环境或趋势构成压力、有改造意图',
39
+ 比和: '两者取向相近、力量相若',
40
+ 被生: '环境或趋势为主体提供支持条件',
41
+ 被克: '环境或趋势对主体构成约束',
42
+ unknown: '五行缺失,关系未定',
43
+ };
44
+
45
+ const findPalace = (r, key) => r.palaces.find((p) => p.star === key || p.gate === key || p.god === key || p.palace === key) || null;
46
+ const cell = (p) => `${p.palace}宫(${p.star}·${p.gate}·${p.god})`;
47
+
48
+ export function interpretQimen(r, opts = {}) {
49
+ if (!r || r.system !== 'qimen') return null;
50
+ const dir = YONG_SHEN[opts.direction] ? opts.direction : 'general';
51
+ const ys = YONG_SHEN[dir];
52
+ const sections = [];
53
+ const unresolved = [];
54
+
55
+ // ① 事实层(全部可直接复算)
56
+ sections.push({
57
+ key: 'facts',
58
+ label: '排盘事实',
59
+ text: `${r.ju};节气 ${r.jieqi}(${r.jieqiTime});${r.siZhu.year}年 ${r.siZhu.month}月 ${r.siZhu.day}日 ${r.siZhu.time}时;`
60
+ + `值符 ${r.zhiFu.star} 落 ${r.zhiFu.gong} 宫,值使 ${r.zhiShi.gate} 落 ${r.zhiShi.gong} 宫;`
61
+ + `旬首 ${r.xunshou};空亡 ${r.kongWang.join('') || '—'};驿马 ${r.ma.zhi || '—'}。`
62
+ + (r.juMethodNote ? `取局口径:${r.juMethodNote}。` : ''),
63
+ });
64
+
65
+ // ② 单宫解释
66
+ const zhiFuP = findPalace(r, r.zhiFu.gong);
67
+ const zhiShiP = findPalace(r, r.zhiShi.gong);
68
+ if (zhiFuP) {
69
+ sections.push({
70
+ key: 'zhifu',
71
+ label: '值符落宫(单宫)',
72
+ text: `值符 ${r.zhiFu.star} 在 ${cell(zhiFuP)}。事实:宫属${zhiFuP.palaceElement},门属${zhiFuP.gateElement || '—'},星属${zhiFuP.starElement || '—'}。`
73
+ + `传统解释:${STAR_MEAN[zhiFuP.star] || ''};${GATE_MEAN[zhiFuP.gate] || ''};${GOD_MEAN[zhiFuP.god] || ''}。`
74
+ + `此宫可读作当下较易发力的位置,但不等于事情即此定向。`,
75
+ });
76
+ }
77
+ if (zhiShiP) {
78
+ sections.push({
79
+ key: 'zhishi',
80
+ label: '值使落宫(单宫)',
81
+ text: `值使 ${r.zhiShi.gate} 在 ${cell(zhiShiP)}。传统解释:${GATE_MEAN[zhiShiP.gate] || ''}。值使所落之宫常被读作事情推进的口子。`,
82
+ });
83
+ }
84
+
85
+ // ③ 用神候选
86
+ const yongList = [];
87
+ for (const key of [...ys.primary, ...ys.secondary]) {
88
+ const p = findPalace(r, key);
89
+ if (p) yongList.push({ key, palace: p });
90
+ }
91
+ if (!yongList.length) unresolved.push(`${ys.label}的用神未上盘`);
92
+ if (yongList.length > 1) unresolved.push(`用神有 ${yongList.length} 个候选(${yongList.map((x) => x.key).join('、')})`);
93
+ sections.push({
94
+ key: 'yongshen',
95
+ label: `用神候选(${ys.label})`,
96
+ text: `${ys.note}。` + (yongList.length
97
+ ? yongList.map((x) => `${x.key} → ${cell(x.palace)}`).join(';') + '。'
98
+ : '本盘未见对应符号,暂以值符落宫参照,属未决项。'),
99
+ });
100
+
101
+ // ④ 两宫关系(用神宫 × 值符宫)
102
+ const primary = yongList[0] || null;
103
+ if (primary && zhiFuP) {
104
+ if (primary.palace.palace === zhiFuP.palace) {
105
+ sections.push({
106
+ key: 'relation',
107
+ label: '用神宫 × 值符宫',
108
+ text: `用神与值符同处 ${zhiFuP.palace} 宫(同宫)——关系并不因此消失,需看该宫门星神与旺衰;`
109
+ + `该宫门宫关系为「${(zhiFuP.gateRelation || gatePalaceRelation(zhiFuP.gate, zhiFuP.palace)).type}」。`,
110
+ });
111
+ } else {
112
+ const rel = wuxingRelation(primary.palace.palaceElement, zhiFuP.palaceElement);
113
+ sections.push({
114
+ key: 'relation',
115
+ label: '用神宫 × 值符宫',
116
+ text: `用神宫(${primary.palace.palace},${primary.palace.palaceElement})对值符宫(${zhiFuP.palace},${zhiFuP.palaceElement})为「${rel}」:${REL_TEXT[rel] || '关系未定'}。`
117
+ + `这是落宫五行层面的关系;是否占优还要看门、月令与所问之事。`,
118
+ });
119
+ }
120
+ }
121
+
122
+ // ⑤ 宫位条件:门迫严格同宫,宫制门单列
123
+ const forced = [];
124
+ const controlled = [];
125
+ for (const p of r.palaces) {
126
+ const rel = p.gateRelation || gatePalaceRelation(p.gate, p.palace);
127
+ if (rel.type === '门迫') forced.push(`${p.palace}宫(${p.gate}${rel.gate}克宫${rel.palace})`);
128
+ else if (rel.type === '宫制门') controlled.push(`${p.palace}宫(宫${rel.palace}克${p.gate}${rel.gate})`);
129
+ }
130
+ const kongCells = r.palaces.filter((p) => p.kong).map((p) => `${p.palace}宫`);
131
+ sections.push({
132
+ key: 'conditions',
133
+ label: '宫位条件',
134
+ text: `门迫(同宫内门克宫):${forced.length ? forced.join('、') : '本盘无'};`
135
+ + `宫制门(宫克门,属「制」不为迫):${controlled.length ? controlled.join('、') : '无'}。`
136
+ + (kongCells.length ? `空亡:${kongCells.join('、')}——落空之宫事多不实。` : '无落空之宫。')
137
+ + `门迫只作宫位条件,不自动等同于用神受制。`,
138
+ });
139
+
140
+ // ⑥ 条件式倾向
141
+ if (unresolved.length) {
142
+ sections.push({
143
+ key: 'tendency',
144
+ label: '最终倾向',
145
+ text: `暂不给单一倾向。未决项:${unresolved.join(';')}。先把用神定下来,再谈顺逆。`,
146
+ });
147
+ } else {
148
+ const rel = wuxingRelation(primary.palace.palaceElement, zhiFuP.palaceElement);
149
+ const tone = rel === '被生' || rel === '生' ? '偏向顺' : rel === '被克' ? '偏向滞' : '中性';
150
+ sections.push({
151
+ key: 'tendency',
152
+ label: '最终倾向',
153
+ text: `在当前输入、规则集与所问之事下:若以「${primary.key}」为用神,用神宫对值符宫为「${rel}」,整体${tone};`
154
+ + `若${ys.label}实际取另一个候选,或时间范围不同,解读边界随之变化。未决项:月令旺衰未纳入本规则集。`,
155
+ });
156
+ }
157
+
158
+ // ⑦ 复核清单
159
+ sections.push({
160
+ key: 'checklist',
161
+ label: '复核清单',
162
+ text: '建议人工核对:①出生时间与真太阳时是否一致;②取局口径(拆补/置闰/超接)是否与所用流派一致;'
163
+ + '③用神是否取对;④空亡宫是否影响关键符号;⑤若结论与现实直觉冲突,先查输入与规则。',
164
+ });
165
+
166
+ return {
167
+ system: 'qimen',
168
+ moduleType: 'divination',
169
+ question: opts.question || '',
170
+ direction: dir,
171
+ sections,
172
+ unresolved,
173
+ ruleset: { juMethod: r.juMethod, schema: 'interpret/2' },
174
+ disclaimer: '奇门属传统术数,用于整理思路与排序选项;不构成对现实的确定性预测,重要决策请以事实与专业意见为准。',
175
+ };
176
+ }