dsh-metaphysics 0.58.0 → 0.59.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.
@@ -0,0 +1,303 @@
1
+ // 奇门问事解读提示:把「先问清事 → 再取用神 → 看落宫与组合 → 回到现实核对」固化成一段提示。
2
+ //
3
+ // 为什么要有这个文件:奇门的盘面符号极多(九宫、八门、九星、八神、天地盘干、十干应克),
4
+ // 模型拿到一屏数据后最省力的写法就是「开门临生门,吉」——这不是解读,是把符号念一遍。
5
+ // 所以提示里把「取哪个符号、为什么取它、它落在哪、和谁什么关系、回到现实查什么」写成硬步骤,
6
+ // 并要求先给人话结论,再给依据。文案与取法分开存,取法可测,文案可改。
7
+ export const QIMEN_PROMPT_VERSION = 'qimen-1';
8
+
9
+ /** 九宫序号 → 方位与领域(读「事情落在哪个领域」时用,不直接当吉凶) */
10
+ export const PALACE_FIELD = {
11
+ 1: { dir: '正北', field: '坎宫:向内、暗中、流动、基础' },
12
+ 2: { dir: '西南', field: '坤宫:承载、家庭、土地、执行方' },
13
+ 3: { dir: '正东', field: '震宫:启动、动作、速度、竞争' },
14
+ 4: { dir: '东南', field: '巽宫:沟通、文书、渐进、反复' },
15
+ 5: { dir: '正中', field: '中宫:枢纽;无门无神,按寄宫(多寄坤二)取用,读时要说清是寄宫' },
16
+ 6: { dir: '西北', field: '乾宫:上级、规则、外部权威、主导方' },
17
+ 7: { dir: '正西', field: '兑宫:口舌、谈判、交易、愉悦' },
18
+ 8: { dir: '东北', field: '艮宫:停滞、门槛、转向、积累' },
19
+ 9: { dir: '正南', field: '离宫:显露、名声、扩散、明亮' },
20
+ };
21
+
22
+ /** 八门的现实含义。门名只是「这件事的形态」,不是吉凶判决 */
23
+ export const GATE_MEANING = {
24
+ 开门: '启动、公开、对外推进、正式场合',
25
+ 休门: '休息、等待、缓和、暂缓',
26
+ 生门: '生长、收益、资源、可持续',
27
+ 伤门: '损耗、冲突、风险、代价',
28
+ 杜门: '阻碍、隐藏、不宜张扬、需要绕过',
29
+ 景门: '信息、名声、曝光、文书往来',
30
+ 死门: '停止、收尾、不可再推、需要换路',
31
+ 惊门: '变动、惊吓、口舌、突发',
32
+ };
33
+
34
+ /**
35
+ * 问事类型 → 用神清单。subjects 的顺序就是「先讲清对方最关心哪个对象」的顺序:
36
+ * 第一个是本方(我),第二个通常是对方/事体,后面才是环境与条件。
37
+ * how 是取法(可复算),why 是取这个符号的理由(三追问第一问要能答出来)。
38
+ */
39
+ export const ASK_KINDS = {
40
+ career: {
41
+ label: '工作 / 事业',
42
+ gate: '开门',
43
+ subjects: [
44
+ { key: 'self', name: '我', how: 'dayStem', why: '日干代表提问者本人;先定位「我现在在哪一宫」,才谈得上后面谁克谁。' },
45
+ { key: 'job', name: '这份工作本身', how: 'gate:开门', why: '开门的本义是「开启、公开、正式」,对应岗位与正式机会,所以用它代表工作这个对象,而不是代表结果好坏。' },
46
+ { key: 'boss', name: '上司 / 决策人', how: 'zhiFu', why: '值符是全局主导之神,代表有拍板权的一方;谈成不成,最终看这一宫的状态与它和日干宫的关系。' },
47
+ { key: 'peer', name: '同事 / 竞争者', how: 'monthStem', why: '月干为同辈;同岗竞争、内部评价这类事落在这里。' },
48
+ ],
49
+ },
50
+ love: {
51
+ label: '感情 / 关系',
52
+ gate: '休门',
53
+ subjects: [
54
+ { key: 'self', name: '我', how: 'dayStem', why: '日干为我。' },
55
+ { key: 'other', name: '对方', how: 'hourStem', why: '时干代表所问之事与对方;感情里问「他怎么样」,时干就是那个人此刻的状态。' },
56
+ { key: 'bond', name: '你俩的连接', how: 'god:六合', why: '六合主结合、撮合、连结;看连接本身有没有被冲开或合住,比看单方态度更接近真相。' },
57
+ ],
58
+ },
59
+ business: {
60
+ label: '生意 / 合作',
61
+ gate: '生门',
62
+ subjects: [
63
+ { key: 'self', name: '我方', how: 'dayStem', why: '日干为我方。' },
64
+ { key: 'partner', name: '对方 / 合作方', how: 'hourStem', why: '时干代表对方与所谈之事。' },
65
+ { key: 'chance', name: '项目机会', how: 'gate:生门', why: '生门主生长与收益,用它代表「这个机会能不能长出东西」。' },
66
+ { key: 'resource', name: '资源与条件', how: 'gate:开门', why: '开门代表公开的资源、渠道、正式手续;有没有真资源,看这一宫有没有被制。' },
67
+ { key: 'deal', name: '契约 / 撮合', how: 'god:六合', why: '六合主合约与中间人;合作最怕的是「六合落空或受冲」,即说了不算。' },
68
+ ],
69
+ },
70
+ action: {
71
+ label: '某件事能不能推进',
72
+ gate: '开门',
73
+ subjects: [
74
+ { key: 'self', name: '我', how: 'dayStem', why: '日干为我。' },
75
+ { key: 'act', name: '这件事本身', how: 'hourStem', why: '时干为事体:你要问的那件事,当下的状态在时干这一宫。' },
76
+ { key: 'env', name: '外部条件', how: 'zhiShi', why: '值使门是「当下正在值班的门」,代表这件事所处的执行环境与节奏。' },
77
+ ],
78
+ },
79
+ day: {
80
+ label: '挑日子 / 看时机',
81
+ gate: '值使',
82
+ subjects: [
83
+ { key: 'act', name: '要办的事', how: 'hourStem', why: '时干为事体,先看它落在哪一宫、受不受制。' },
84
+ { key: 'env', name: '当天的执行环境', how: 'zhiShi', why: '值使门代表当值之门,是这一天的主导形态,不把它当吉凶。' },
85
+ { key: 'self', name: '办事的人', how: 'dayStem', why: '日干为办事人;人有空亡、受制,日子再好也执行不了。' },
86
+ ],
87
+ },
88
+ other: {
89
+ label: '其他具体事',
90
+ gate: '开门',
91
+ subjects: [
92
+ { key: 'self', name: '我', how: 'dayStem', why: '日干为我。' },
93
+ { key: 'act', name: '所问之事', how: 'hourStem', why: '时干为事体。' },
94
+ { key: 'env', name: '当下环境', how: 'zhiShi', why: '值使门代表当下的执行节奏。' },
95
+ ],
96
+ },
97
+ };
98
+
99
+ /** 回到现实要核对什么:按类型给具体清单,不写「请结合现实」这种空话 */
100
+ export const REALITY_CHECK = {
101
+ career: ['有没有书面邀约或正式通知?(口头承诺不算)', '岗位职责、汇报对象、试用期写清楚了吗?', '薪酬范围、发放方式、社保口径有没有落到纸面?', '最坏情况是什么:试用不过、岗位撤销,你损失多少?'],
102
+ love: ['对方有没有具体行动:约见、介绍给朋友、共同计划?', '这种投入持续了多久,还是一阵热?', '有没有第三方牵涉(前任、家人、异地)?', '你自己愿意为这段关系让渡什么,底线在哪?'],
103
+ business: ['资源清单能落到具体人、具体钱、具体渠道吗?', '合同条款:出资、分账、违约、退出,哪条没写?', '谁执行?执行人是全职还是顺手帮忙?', '如果对方撤了,你一个人能不能收场?'],
104
+ action: ['这件事有没有截止日期?谁定的?', '实际成本:钱、时间、人情,各多少?', '做错了能不能退回去?可逆性有多高?', '成功的样子长什么样,能不能用一句话说清?'],
105
+ day: ['当天必须到场的人确认了吗?', '场地、材料、审批这些硬约束排除了吗?', '有备选日期吗?排到第几位?', '这一天之前的准备来得及吗?'],
106
+ other: ['这件事成功的样子,能用一件可观察的事描述吗?', '有没有一个明确的时间点可以验证?', '如果没成,你会先怀疑哪一环?', '现在最该做的一件小事是什么?'],
107
+ };
108
+
109
+ /** 少用术语、面向提问者的表达要求 */
110
+ export const STYLE_RULES = [
111
+ '开头先用三到五句话回答他问的那件事,全用日常说法:不出现「用神、落宫、值符、生克」这些词。',
112
+ '每个术语第一次出现时,必须紧跟一句人话解释(例:「开门,就是这件事能不能摆到台面上正式推进」)。',
113
+ '不写「吉/凶」二字作结论,改写成人能执行的话:能推进 / 卡在谁那里 / 还缺什么条件 / 大概什么时候会有信号。',
114
+ '盘面给不出的东西(具体金额、对方心里怎么想、某天几点),直接说给不出,不要补。',
115
+ ];
116
+
117
+ const HARD_RULES = [
118
+ '只使用下面「盘面事实」里出现的符号。没给的宫、门、星、神,一律不许自己补。',
119
+ '不许说「这是生门所以好」「这是白虎所以凶」。门名与神名只说明形态,吉凶要看它和日干宫、时干宫的关系,以及有没有空亡、受制、入墓。',
120
+ '不给医疗、法律、投资的具体结论;这些只能建议去找对应专业人士。',
121
+ '不给公历日期。奇门这里只谈「什么条件下会有信号」,不谈「几月几号」。',
122
+ '不确定就写不确定,并说明缺哪条信息才能确定。',
123
+ ];
124
+
125
+ const stemOf = (siZhu, key) => {
126
+ const v = siZhu && siZhu[key];
127
+ return v ? String(v).charAt(0) : '';
128
+ };
129
+
130
+ /** 天盘某干落在哪一宫(日干/时干/月干定位都走这里) */
131
+ function palaceOfStem(q, stem) {
132
+ if (!stem) return null;
133
+ const hit = (q.palaces || []).find((p) => p.tianPan === stem);
134
+ if (hit) return hit;
135
+ // 中五宫不在 palaces 里(它没有门、不参与门宫关系),天盘干可能只在这里。
136
+ // 中宫按「寄坤二宫」取,但要把「这是中宫」说出来,不能让读者以为是坤二本宫。
137
+ const c = q.centerPalace;
138
+ return c && c.tianPan === stem ? c : null;
139
+ }
140
+
141
+ function palaceOfGate(q, gate) {
142
+ if (!gate) return null;
143
+ return (q.palaces || []).find((p) => p.gate === gate) || null;
144
+ }
145
+
146
+ function palaceOfGod(q, god) {
147
+ if (!god) return null;
148
+ return (q.palaces || []).find((p) => p.god === god) || null;
149
+ }
150
+
151
+ /** 把 subjects 里的取法解析成 { palace, symbol };解析不到就明确返回缺失原因 */
152
+ export function resolveSubject(q, sub) {
153
+ const si = q.siZhu || {};
154
+ const how = String(sub.how || '');
155
+ if (how === 'dayStem') {
156
+ const s = stemOf(si, 'day');
157
+ return { symbol: `日干 ${s}`, palace: palaceOfStem(q, s), missing: s ? '' : '四柱里没有日干' };
158
+ }
159
+ if (how === 'hourStem') {
160
+ const s = stemOf(si, 'time');
161
+ return { symbol: `时干 ${s}`, palace: palaceOfStem(q, s), missing: s ? '' : '四柱里没有时干' };
162
+ }
163
+ if (how === 'monthStem') {
164
+ const s = stemOf(si, 'month');
165
+ return { symbol: `月干 ${s}`, palace: palaceOfStem(q, s), missing: s ? '' : '四柱里没有月干' };
166
+ }
167
+ if (how === 'zhiFu') {
168
+ const g = q.zhiFu && q.zhiFu.gong;
169
+ return { symbol: `值符(${(q.zhiFu && q.zhiFu.star) || ''})`, palace: (q.palaces || []).find((p) => p.palace === String(g)) || null, missing: g ? '' : '没有值符落宫' };
170
+ }
171
+ if (how === 'zhiShi') {
172
+ const g = q.zhiShi && q.zhiShi.gong;
173
+ return { symbol: `值使(${(q.zhiShi && q.zhiShi.gate) || ''})`, palace: (q.palaces || []).find((p) => p.palace === String(g)) || null, missing: g ? '' : '没有值使落宫' };
174
+ }
175
+ if (how.startsWith('gate:')) {
176
+ const gate = how.slice(5);
177
+ const p = palaceOfGate(q, gate);
178
+ return { symbol: `${gate}`, palace: p, missing: p ? '' : `盘上没有${gate}` };
179
+ }
180
+ if (how.startsWith('god:')) {
181
+ const god = how.slice(4);
182
+ const p = palaceOfGod(q, god);
183
+ return { symbol: `${god}`, palace: p, missing: p ? '' : `盘上没有${god}` };
184
+ }
185
+ return { symbol: how, palace: null, missing: `未知取法 ${how}` };
186
+ }
187
+
188
+ /** 一宫的事实摘要:只写盘面上有的,不做判断 */
189
+ function palaceFacts(p) {
190
+ if (!p) return '(这一项在盘上定位不到)';
191
+ const f = PALACE_FIELD[p.palace] || { dir: '', field: '' };
192
+ const bits = [
193
+ `${p.palace} 宫(${f.dir})`,
194
+ p.center ? '中宫(无门无神,按寄宫取用)' : `门:${p.gate || '—'}${p.gateRelation && p.gateRelation.type ? `(${p.gateRelation.type}:${p.gateRelation.note || ''})` : ''}`,
195
+ `星:${p.star || '—'}`,
196
+ p.center ? '' : `神:${p.god || '—'}`,
197
+ `天盘 ${p.tianPan || '—'} / 地盘 ${p.diPan || '—'}${p.stemRelation && p.stemRelation.type ? `(${p.stemRelation.type})` : ''}`,
198
+ ];
199
+ if (p.kong) bits.push('空亡');
200
+ if (p.ma) bits.push('临马星');
201
+ if (p.ganYing && p.ganYing.name) bits.push(`十干应克:${p.ganYing.name}`);
202
+ return bits.join(' ');
203
+ }
204
+
205
+ const CHECK_QUESTIONS = [
206
+ '你说「我」是哪个符号?为什么是它,不是别的?',
207
+ '机会在哪一宫、阻碍在哪一宫、对方态度在哪一宫?各自依据哪一条关系(生、克、合、冲、空、墓)?',
208
+ '盘上有没有相反提示?你为什么采信这一边,而不是那一边?',
209
+ ];
210
+
211
+ /**
212
+ * 生成一段可直接交给模型的提示。
213
+ * @param {object} q 奇门盘(compute 结果里的 qimen 部分,或整个 r)
214
+ * @param {{question?:string, kind?:string, timeKnown?:boolean, askedAt?:string}} opts
215
+ * @returns {{version:string, kind:string, blocked:boolean, text:string, subjects:Array}}
216
+ */
217
+ export function buildQimenPrompt(q, opts = {}) {
218
+ const chart = q && q.palaces ? q : (q && q.qimen);
219
+ const kindKey = ASK_KINDS[opts.kind] ? opts.kind : 'other';
220
+ const kind = ASK_KINDS[kindKey];
221
+ const question = String(opts.question || '').replace(/\s+/g, ' ').trim().slice(0, 200);
222
+
223
+ // 第一步永远是时间:奇门以「起局那一刻」定盘,时刻不明就没有盘,不能默认拿现在起。
224
+ if (opts.timeKnown === false || !chart) {
225
+ return {
226
+ version: QIMEN_PROMPT_VERSION,
227
+ kind: kindKey,
228
+ blocked: true,
229
+ subjects: [],
230
+ text: [
231
+ '先别起局——时间还不确定。',
232
+ '',
233
+ '奇门是按「起局那一刻」排盘的,时刻差一个时辰,九宫里的门、星、神全换一遍。所以先确认:',
234
+ '1)这盘是按现在这一刻起,还是按你打算行动的那一刻起?',
235
+ '2)你说的日期时间,是钟表时间还是农历?在哪个城市(真太阳时会差几分钟到十几分钟)?',
236
+ '',
237
+ '确认之后,再用同一句话把要问的事说清楚:主语 + 时间 + 可验证的结果,例如「下周三之前,这份工作的书面 offer 能不能拿到」。',
238
+ '在那之前,我不排盘,也不猜。',
239
+ ].join('\n'),
240
+ };
241
+ }
242
+
243
+ const subjects = kind.subjects.map((s) => {
244
+ const got = resolveSubject(chart, s);
245
+ return { ...s, symbol: got.symbol, palace: got.palace, missing: got.missing };
246
+ });
247
+
248
+ const si = chart.siZhu || {};
249
+ const head = [
250
+ `# 奇门问事解读(提示版本 ${QIMEN_PROMPT_VERSION})`,
251
+ '',
252
+ '## 你要做的',
253
+ '先把这张盘翻译成「他能拿去用的话」,再给依据。顺序不能反:先结论(人话)→ 再盘面依据 → 再现实核对 → 最后说哪里不确定。',
254
+ '',
255
+ '## 一、先确认问的是哪一件事',
256
+ question
257
+ ? `他的问题是:「${question}」`
258
+ : '他还没说清要问什么。第一句先问清:要问的是哪一件具体的事(哪份工作、哪次沟通、哪笔合作),以及他希望看到什么结果。不要笼统答「运势如何」。',
259
+ `已按「${kind.label}」取用神。若他实际问的是别的事,先纠正类型再解读,不要硬套。`,
260
+ '',
261
+ '## 二、盘面事实(只能用这些,不许自行补算)',
262
+ `四柱:${si.year || '—'}年 ${si.month || '—'}月 ${si.day || '—'}日 ${si.time || '—'}时;局:${chart.dun || ''}${chart.juNumber || ''}局(${chart.yuan || ''});值符 ${(chart.zhiFu && chart.zhiFu.star) || '—'} 落 ${(chart.zhiFu && chart.zhiFu.gong) || '—'} 宫;值使 ${(chart.zhiShi && chart.zhiShi.gate) || '—'} 落 ${(chart.zhiShi && chart.zhiShi.gong) || '—'} 宫。`,
263
+ (chart.kongWang && chart.kongWang.length) ? `空亡:${chart.kongWang.join('、')}。` : '',
264
+ '',
265
+ '## 三、取哪个符号代表什么(每一项都要能说出「为什么是它」)',
266
+ ...subjects.map((s, i) => `${i + 1}. ${s.name} → ${s.symbol}${s.missing ? `(注意:${s.missing})` : ''}\n - 取法理由:${s.why}\n - 盘面:${palaceFacts(s.palace)}`),
267
+ '',
268
+ '## 四、怎么读状态(这是最容易偷懒的地方)',
269
+ '1. 落宫先说「这件事落在哪个领域」:',
270
+ ...subjects.filter((s) => s.palace).map((s) => ` - ${s.name}:${(PALACE_FIELD[s.palace.palace] || {}).field || ''}`),
271
+ '2. 所遇之门说明「这件事现在是什么形态」,不是吉凶:',
272
+ ...Object.entries(GATE_MEANING).map(([g, mean]) => ` - ${g}:${mean}`),
273
+ '3. 关系只认四种,且都要指到具体两宫:',
274
+ ' - 生:得助(谁生谁,说清方向)',
275
+ ' - 克:受制(谁克谁,说清方向)',
276
+ ' - 合:连接、聚合(合住也可能意味着被拴住)',
277
+ ' - 冲:变动、对立、打破(冲开也可能意味着解脱)',
278
+ '4. 空亡、入墓、门迫、击刑这些「减分项」,要么明确说它压低了什么,要么别提——不要念一遍名字就过去。',
279
+ '',
280
+ '## 五、开口之前先自检这三问',
281
+ ...CHECK_QUESTIONS.map((q2, i) => `${i + 1}. ${q2}`),
282
+ '三问答不全,就还没到能下结论的程度,先把答不出的那条写出来。',
283
+ '',
284
+ '## 六、最后必须回到现实核对',
285
+ ...REALITY_CHECK[kindKey].map((x, i) => `${i + 1}. ${x}`),
286
+ '盘面只能给方向;上面这些是「他明天可以去查、去问、去要」的东西。核对完再回来谈结论。',
287
+ '',
288
+ '## 七、表达要求',
289
+ ...STYLE_RULES.map((x) => `- ${x}`),
290
+ '',
291
+ '## 八、禁止',
292
+ ...HARD_RULES.map((x) => `- ${x}`),
293
+ '',
294
+ '## 九、输出格式',
295
+ '1. **先给你要的答案**:三到五句人话,直接回答他问的那件事。',
296
+ '2. **盘上依据**:按「我 / 对方或事体 / 环境」三块列,每块一句符号 + 一句人话解释。',
297
+ '3. **现实里该去核对什么**:抄上面第六节里适用的项,不要全抄。',
298
+ '4. **相反提示与不确定处**:盘上哪些地方指向另一个方向,你为什么这样权衡。',
299
+ '5. **一句提醒**:这是参考,不替代你自己去核实和决定。',
300
+ ].filter((x) => x !== '').join('\n');
301
+
302
+ return { version: QIMEN_PROMPT_VERSION, kind: kindKey, blocked: false, subjects, text: head };
303
+ }
package/engine/rectify.js CHANGED
@@ -96,7 +96,8 @@ export function rectifyBirth(opts) {
96
96
  hour,
97
97
  label: `${String(hour).padStart(2, '0')}:00 起`,
98
98
  dayGanzhi: chart.pillars.day.ganzhi,
99
- hourGanzhi: chart.pillars.hour.ganzhi,
99
+ // 时辰未知时没有时柱可校正:给 null,让调用方自己判断要不要做这件事
100
+ hourGanzhi: chart.pillars.hour ? chart.pillars.hour.ganzhi : null,
100
101
  score: clean.length ? matched.length / clean.length : 0,
101
102
  hits: matched.length,
102
103
  total: clean.length,
package/engine/trend.js CHANGED
@@ -145,6 +145,9 @@ export function buildTrend(input) {
145
145
  dayMaster: `${dayStem}(${r.dayMaster.yinYang}${r.dayMaster.element})`,
146
146
  strength: { level: r.strength.level, score: r.strength.score, boundary },
147
147
  toAge,
148
+ // 当前年份由调用方给(asOf):引擎不读挂钟,同一条线在任意时刻复算都一致。
149
+ // 没给就不写这个键,summary 那边据此省掉「你现在在第几步」。
150
+ ...(Number.isInteger(input.nowYear) ? { nowYear: input.nowYear } : {}),
148
151
  ...(boundary
149
152
  ? {
150
153
  boundaryNote:
@@ -0,0 +1,208 @@
1
+ // 六爻应期:把《增删卜易》各门类应期总注的条文逐条落成规则。
2
+ //
3
+ // 条文出处:各門類應期總注章第又二十六(本地底本见 test/fixtures/citation-sources.json,
4
+ // 键 https://zh.wikisource.org/wiki/增刪卜易/26又3)。原文 19 条,这里只实现能落到「地支」上的那些;
5
+ // 落不到的(如「独发独静」「卦之不明再占」)明确列在 NOT_IMPLEMENTED,不假装做了。
6
+ //
7
+ // 设计原则(和 yingqiWindow 一致):只给「该留意哪一支」,不换算公历日期。
8
+ // 一条卦例往往同时满足两条规则,所以输出「适用规则列表」+「触发支并集」,
9
+ // 并标出优先级最高的一条作主规则——对拍时分会算「并集命中」与「主规则命中」两个数,
10
+ // 不拿并集去冒充精度。
11
+ import { chongOf, heOf, jinTuiOf, zhiElementOf } from './liuyao.js';
12
+
13
+ const ZHI = '子丑寅卯辰巳午未申酉戌亥'.split('');
14
+
15
+ // 五行墓库:水/土墓辰、木墓未、火墓戌、金墓丑
16
+ const MU_OF = { 水: '辰', 土: '辰', 木: '未', 火: '戌', 金: '丑' };
17
+ // 五行生我
18
+ const SHENG_ME = { 木: '水', 火: '木', 土: '火', 金: '土', 水: '金' };
19
+ const SHENG_BY_ME = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
20
+ const KE_ME = { 木: '金', 火: '水', 土: '木', 金: '火', 水: '土' };
21
+
22
+ /** 某五行对应的全部地支(金→申酉) */
23
+ const BRANCHES_OF = (() => {
24
+ const out = {};
25
+ for (const b of ZHI) {
26
+ const e = zhiElementOf(b);
27
+ (out[e] = out[e] || []).push(b);
28
+ }
29
+ return out;
30
+ })();
31
+
32
+ export const YINGQI_RULE_IDS = [
33
+ 'jing-zhi-chong', 'dong-he-zhi', 'taiwang-mu-chong', 'shuaijue-sheng-wang',
34
+ 'rumu-chong-kai', 'liuhe-xiang-ji', 'yuepo-xi-tian', 'xunkong-tian-chong',
35
+ 'jixiang-keshen-shouke', 'xiong-keshen-fengsheng', 'huajin-zhi-he',
36
+ 'huatui-ji-zhi-chong', 'bianyao-dongyao', 'shikong-yuanshen-zhi', 'shishuai-yuanshen-chong',
37
+ ];
38
+
39
+ /**
40
+ * 规则的排列顺序 = 原文《各門類應期總注章第又二十六》19 条的罗列次序。
41
+ *
42
+ * 这一条要写清楚,因为 0.59.0 之前这里是错的:
43
+ * 旧注释称「状态互斥优先级照应期总注的排列:空 → 破 → 动 → 墓 → 太旺 → 衰绝 → 静」,
44
+ * 但把原文逐条对下来,真实次序是——
45
+ * 静 → 动 → 太旺 → 衰绝 → 入墓 → 六合 → 月破 → 旬空 → 大象吉受克 → 大象凶受克
46
+ * → 元神总纲 → 化进 → 化退 → 独发独静 → 变爻动爻 → 远近当分 → 再占 → 世空元动 → 世衰元静
47
+ * 旧代码几乎是把这串倒过来排的,而「空 → 破 → … → 静」这个说法原文里没有,是编的。
48
+ *
49
+ * 更要紧的一点:**原文是分类罗列,不是效力排序**。前八条是「常、常、病×6」,
50
+ * 后面又混进化进/化退与变爻动爻,章内找不到任何「哪条先判」的表述。
51
+ * 所以 primary 只表示「按原文顺序第一条生效的规则」,不表示「最可能应验的那条」——
52
+ * 对拍(tools/backtest-yingqi.mjs)也显示按这个顺序取第一条,命中率与随机基线无显著差异。
53
+ */
54
+ export const RULE_ORDER = [
55
+ 'jing-zhi-chong',
56
+ 'dong-he-zhi',
57
+ 'taiwang-mu-chong',
58
+ 'shuaijue-sheng-wang',
59
+ 'rumu-chong-kai',
60
+ 'liuhe-xiang-ji',
61
+ 'yuepo-xi-tian',
62
+ 'xunkong-tian-chong',
63
+ 'jixiang-keshen-shouke',
64
+ 'xiong-keshen-fengsheng',
65
+ 'huajin-zhi-he',
66
+ 'huatui-ji-zhi-chong',
67
+ 'bianyao-dongyao',
68
+ 'shikong-yuanshen-zhi',
69
+ 'shishuai-yuanshen-chong',
70
+ ];
71
+
72
+ // 条文里给出、但落不到「某支」上的条目:列出来,避免被误以为已经做了
73
+ export const NOT_IMPLEMENTED = [
74
+ '問有應於獨發獨靜:詳獨發章(要判「獨發/獨靜」整卦只有一爻動,属整卦层,未并入单爻规则)',
75
+ '勿謂爻之不驗,遠近當分:遠事定之以年月,近事應之於日時(要按事情远近换尺,本模块不猜远近)',
76
+ '倘遇卦之不明,再占是一法(操作建议,不是应期取法)',
77
+ '元神來助來扶,貴看用神衰旺,忌神來沖克,須觀元气興衰(总纲,无独立取支)',
78
+ ];
79
+
80
+ const uniq = (xs) => [...new Set(xs.filter(Boolean))];
81
+
82
+ /**
83
+ * 判用神当前状态。
84
+ *
85
+ * 各项**独立计算、不互斥**——同一爻可以既旬空又月破、又入墓,各条规则各自判断要不要给支。
86
+ * 0.59.0 之前的注释写成「状态互斥优先级:空 → 破 → 动 → 墓 → 太旺 → 衰绝 → 静」,
87
+ * 原文里没有这层互斥关系,那段话是编的;这里没有选择逻辑,所以改注释不改行为。
88
+ * @param {object} line 用神爻(buildLiuyao 的 ben.lines 之一)
89
+ * @param {object} bianLine 同位变爻(不动则为 null)
90
+ * @param {{monthBranch?:string, dayBranch?:string, kong?:string[], sameElement?:number}} ctx
91
+ */
92
+ export function yingqiState(line, bianLine, ctx = {}) {
93
+ const branch = line.branch;
94
+ const element = line.element || zhiElementOf(branch);
95
+ const month = ctx.monthBranch || null;
96
+ const day = ctx.dayBranch || null;
97
+ const kong = ctx.kong || [];
98
+ const kongHit = kong.includes(branch);
99
+ const monthPo = !!(month && chongOf(branch) === month);
100
+ const moving = Boolean(line.moving);
101
+ const bianBranch = bianLine ? bianLine.branch : null;
102
+ const jt = bianBranch ? jinTuiOf(branch, bianBranch) : null;
103
+ const muBranch = MU_OF[element] || null;
104
+ const ruMu = !!(muBranch && (day === muBranch || month === muBranch));
105
+ // 六合:与月建或日辰作合,或动而化合(变爻与本爻六合)
106
+ const heWith = [];
107
+ if (month && heOf(branch) === month) heWith.push({ with: '月建', branch: month });
108
+ if (day && heOf(branch) === day) heWith.push({ with: '日辰', branch: day });
109
+ if (moving && bianBranch && heOf(branch) === bianBranch) heWith.push({ with: '变爻', branch: bianBranch });
110
+ // 太旺:月建同气(旺/相)且日辰生扶或比和
111
+ const monthTone = month ? (BRANCHES_OF[zhiElementOf(month)] || []) : [];
112
+ const monthSame = !!(month && zhiElementOf(month) === element);
113
+ const monthSheng = !!(month && zhiElementOf(month) === SHENG_ME[element]);
114
+ const daySame = !!(day && zhiElementOf(day) === element);
115
+ const daySheng = !!(day && zhiElementOf(day) === SHENG_ME[element]);
116
+ const taiWang = (monthSame || monthSheng) && (daySame || daySheng);
117
+ // 衰绝:被月建所克,且日辰不生不扶
118
+ const monthKe = !!(month && zhiElementOf(month) === KE_ME[element]);
119
+ const shuaiJue = monthKe && !(daySame || daySheng);
120
+
121
+ return {
122
+ branch, element, moving, bianBranch, jinTui: jt, kongHit, monthPo, ruMu,
123
+ heWith, taiWang, shuaiJue, monthSame, monthSheng, daySame, daySheng, monthKe,
124
+ muBranch, monthSameBranches: monthTone,
125
+ };
126
+ }
127
+
128
+ /**
129
+ * 按应期总注给出触发支集合。
130
+ * @returns {{rules:Array<{id:string,label:string,quote:string,branches:string[]}>, primary:object|null, branches:string[], state:object}}
131
+ */
132
+ export function yingqiPredict(result, useGod, opts = {}) {
133
+ const st = {
134
+ monthBranch: opts.monthBranch || null,
135
+ dayBranch: opts.dayBranch || (result && result.dayGanZhi ? result.dayGanZhi.branch : null),
136
+ kong: Array.isArray(result && result.xunKong) ? result.xunKong : [],
137
+ };
138
+ if (!useGod || !useGod.branch) return { rules: [], primary: null, branches: [], state: null };
139
+
140
+ const bianLine = useGod.position && result && result.bian
141
+ ? (result.bian.lines || []).find((l) => l.position === useGod.position) || null
142
+ : null;
143
+ const s = yingqiState({ branch: useGod.branch, element: useGod.element, moving: useGod.moving }, bianLine, st);
144
+ const b = s.branch;
145
+ const rules = [];
146
+ const add = (id, label, quote, branches) => {
147
+ const bs = uniq(branches);
148
+ if (bs.length) rules.push({ id, label, quote, branches: bs });
149
+ };
150
+
151
+ if (s.kongHit) add('xunkong-tian-chong', '旬空填实/冲空', '旬空最愛塡沖', [b, chongOf(b)]);
152
+ if (s.monthPo) add('yuepo-xi-tian', '月破填实/逢合', '月破喜塡…又有逢午月應之,塡實之月則不破矣', [b, heOf(b)]);
153
+ if (s.moving && s.jinTui === '进') add('huajin-zhi-he', '化进神逢值逢合', '化進神、逢值逢合:如申動酉…有應申月日者,有應巳月日者', [b, heOf(b)]);
154
+ if (s.moving && s.jinTui === '退') add('huatui-ji-zhi-chong', '化退神忌值忌冲', '化退神、忌值忌沖:如酉化申…有應申月日者,有應寅日月者', [s.bianBranch, chongOf(s.bianBranch)]);
155
+ if (s.moving && !s.jinTui) add('bianyao-dongyao', '动爻/变爻之支', '問有應於變爻動爻:如爻臨戌土,變出酉金,有戌日應者,有酉日應者', [b, s.bianBranch]);
156
+ if (s.moving) add('dong-he-zhi', '动而逢合逢值', '動而逢合逢值:如主事爻臨子水發動,後遇丑日子日而應之', [heOf(b), b]);
157
+ if (s.ruMu) add('rumu-chong-kai', '入墓喜冲开', '入三墓俱喜沖開:如主事爻臨午火,假使火墓於戌,後逢辰日則應之', [chongOf(s.muBranch)]);
158
+ if (s.heWith.length) add('liuhe-xiang-ji', '六合宜相击(冲开)', '遇六合亦宜相擊…卽如主象臨子,與丑作合,後逢午未日應之', s.heWith.flatMap((h) => [chongOf(b), chongOf(h.branch)]));
159
+ if (s.taiWang) add('taiwang-mu-chong', '太旺逢墓逢冲', '太旺者逢墓逢沖:如主事爻臨午火…後逢亥子日應之,又有戌日應之乃火入墓也', [s.muBranch, chongOf(b)]);
160
+ if (s.shuaiJue) add('shuaijue-sheng-wang', '衰绝遇生遇旺', '衰絕者,遇生遇旺:如主事爻屬金,占卦於巳午月日…後逢土月日或至秋令當時則旺矣', [...(BRANCHES_OF[SHENG_ME[s.element]] || []), ...(BRANCHES_OF[s.element] || [])]);
161
+ if (!s.moving && !s.kongHit && !s.monthPo && !s.ruMu && !s.heWith.length) {
162
+ add('jing-zhi-chong', '静而逢值逢冲', '靜而逢值逢沖:如主事爻臨子水不動,後逢子日午日而應之', [b, chongOf(b)]);
163
+ }
164
+
165
+ // 元神/忌神两条要整卦信息:用神受克 → 待克神受制;用神衰而元静 → 待元神逢冲
166
+ const lines = (result && result.ben && result.ben.lines) || [];
167
+ const keBranch = (() => {
168
+ const keEl = KE_ME[s.element];
169
+ const hit = lines.filter((l) => (l.element || zhiElementOf(l.branch)) === keEl && l.moving);
170
+ return hit.length ? hit[0].branch : null;
171
+ })();
172
+ const yuanBranch = (() => {
173
+ const yuanEl = SHENG_ME[s.element];
174
+ const hit = lines.filter((l) => ((l.element || zhiElementOf(l.branch)) === yuanEl));
175
+ return hit.length ? hit[0].branch : null;
176
+ })();
177
+ if (keBranch && (s.shuaiJue || s.kongHit || s.monthPo)) {
178
+ add('jixiang-keshen-shouke', '待克神受克', '大象吉而受克,須待克神受克…後逢申酉日沖克克神則吉', [chongOf(keBranch)]);
179
+ }
180
+ if (yuanBranch && s.shuaiJue && !s.moving) {
181
+ add('shishuai-yuanshen-chong', '元神逢冲', '世衰元靜,必然是元气逢沖…後逢巳日成事', [chongOf(yuanBranch)]);
182
+ }
183
+ if (yuanBranch && s.kongHit) {
184
+ add('shikong-yuanshen-zhi', '待元神逢值', '世空元動,須待元神逢值…主亥日得財', [yuanBranch]);
185
+ }
186
+
187
+ // 按原文罗列次序排(见 RULE_ORDER),primary 只是「第一条生效的」,不是「最可能应验的」
188
+ const rankOf = (id) => {
189
+ const i = RULE_ORDER.indexOf(id);
190
+ return i < 0 ? RULE_ORDER.length : i;
191
+ };
192
+ rules.sort((a, b) => rankOf(a.id) - rankOf(b.id));
193
+
194
+ const primary = rules[0] || null;
195
+ return {
196
+ rules,
197
+ primary,
198
+ branches: uniq(rules.flatMap((r) => r.branches)),
199
+ state: s,
200
+ };
201
+ }
202
+
203
+ /** 把预测结果压成一句可读的话(报告与面板共用措辞) */
204
+ export function yingqiSay(pred) {
205
+ if (!pred || !pred.branches.length) return '应期:无可用触发支(缺用神或月建)';
206
+ const main = pred.primary ? pred.primary.label : '无';
207
+ return `应期:主看「${main}」,留意 ${pred.branches.join('、')} 支(共 ${pred.branches.length} 支)`;
208
+ }
package/index.js CHANGED
@@ -36,6 +36,7 @@ const DESCRIPTION = [
36
36
  '要给「结合所问之事的解读」时,把 withInterpret 设为 true,并尽量带上 question(所问之事)与 direction(询问方向)。',
37
37
  '用户用一句话改参数(「换成紫微看 2030 年事业」「女命」「晚子时算次日」「出生在成都」「不用真太阳时」「详细解读一下」)时,把原话放进 revise(可配合 handle),引擎翻译成参数补丁并在结果 revision 里回报改了哪些、没认哪些;不要自己把原话拆成猜测的参数。',
38
38
  '追问同一张盘:结果里有 handle,下次传 handle + 增量即可(例如 { handle, asOf: 2030 }、{ handle, withInterpret: true, question: "何时换工作" }、{ handle, system: "ziwei" } 看同一时间另一套盘),不必让用户重报出生信息;句柄带校验位,截断或改字会报 E_HANDLE。',
39
+ '出生时刻未知时不要编一个钟点:传 hourUnknown: true——八字只排年/月/日三柱(时柱暂缺,摘要首行提示),趋势按日降级,紫微与合盘会直接以 HOUR_UNKNOWN 阻断并给出补时刻的引导。',
39
40
  '结果可复现:tarot / liuyao 传 seed,紫微 / 八字 / 塔罗传 asOf;输出摘要会写明晚子时与真太阳时实际采用的口径。需要回放或对拍时传 shadow: true,结果带影子盘 ID(同参数同 ID;baseId 不含版本,可跨版本比对字段变化)。要看结论是怎么推出来的,传 trace: true:六爻给纳甲→六亲→世应→伏神→旺衰,八字给旺衰分值与格局取用,紫微给格局命中条件,奇门与大六壬给盘面层与取用层;同时回传 traceText(同一份层的 Markdown,可直接贴进对话或文档)。',
40
41
  '结果用于传统文化与娱乐参考,不得用于医疗、投资、法律、生死等确定性断言。',
41
42
  ].join('');
@@ -267,6 +268,10 @@ export function apply(ctx) {
267
268
  type: 'boolean',
268
269
  description: '八字 / 紫微专用:为同一份出生信息再排另一套盘,返回「八字 × 紫微 交叉验证」(同向 / 有张力 / 互补),不合并数值。',
269
270
  },
271
+ hourUnknown: {
272
+ type: 'boolean',
273
+ description: '出生时刻未知(仅 bazi / trend):八字只排年/月/日三柱,时柱不排并标注「时柱暂缺」,摘要首行写「时柱按正午暂算,涉及时柱的结论仅供参考」,起运与交运按日估算;紫微与合盘会以 HOUR_UNKNOWN 阻断。默认 false。',
274
+ },
270
275
  diff: {
271
276
  type: 'boolean',
272
277
  description: '口径对照(bazi / ziwei / qimen):同一组输入分别按当前口径与另一档各排一次,列出根因字段(如日柱、命宫、局数)与连带变化数。只列差异,不判哪套口径更准,也不改盘面。',