dsh-metaphysics 0.62.2 → 0.62.3

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.
@@ -1,13 +1,13 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="470" height="28" role="img" aria-label="公开对拍:0 处不符|128 例 · 5 剔除 · 312 紫微 · 20 奇门 · 12 交运">
2
- <title>公开对拍:0 处不符|128 例 · 5 剔除 · 312 紫微 · 20 奇门 · 12 交运</title>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="455" height="28" role="img" aria-label="公开对拍:0 处不符|128 例 · 5 剔除 · 127 未核 · 312 紫微 · 20 奇门 · 12 交运">
2
+ <title>公开对拍:0 处不符|128 例 · 5 剔除 · 127 未核 · 312 紫微 · 20 奇门 · 12 交运</title>
3
3
  <linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>
4
- <rect width="470" height="28" rx="4" fill="#555"/>
5
- <rect x="62" width="408" height="28" rx="4" fill="#1f6f3f"/>
6
- <rect x="62" width="408" height="28" fill="url(#s)"/>
4
+ <rect width="455" height="28" rx="4" fill="#555"/>
5
+ <rect x="62" width="393" height="28" rx="4" fill="#1f6f3f"/>
6
+ <rect x="62" width="393" height="28" fill="url(#s)"/>
7
7
  <rect width="66" height="28" rx="4" fill="#555"/>
8
8
  <rect x="62" width="4" height="28" fill="#555"/>
9
9
  <g fill="#fff" text-anchor="middle" font-family="Verdana,DejaVu Sans,sans-serif" font-size="11">
10
10
  <text x="33" y="18">对拍</text>
11
- <text x="266" y="18">0 处不符|128 例 · 5 剔除 · 312 紫微 · 20 奇门 · 12 交运</text>
11
+ <text x="258.5" y="18">0 处不符|128 例 · 5 剔除 · 127 未核 · 312 紫微 · 20 奇门 · 12 交运</text>
12
12
  </g>
13
13
  </svg>
package/engine/almanac.js CHANGED
@@ -10,7 +10,7 @@
10
10
  import { solarToLunar } from './lunar.js';
11
11
  import { nayinOf } from './nayin.js';
12
12
  import {
13
- toJDN, fromJDN, dayGanzhiIndex, ganzhiName, termWindow, termInstantJd, partsToJd, STEMS, BRANCHES, TERM_NAMES,
13
+ toJDN, fromJDN, dayGanzhiIndex, ganzhiName, termWindow, termInstantJd, STEMS, BRANCHES, TERM_NAMES,
14
14
  } from './calendar.js';
15
15
  import { HEX64 } from './interpret-data.js';
16
16
  import { sanheGroupOf } from './sanhe.js';
@@ -196,13 +196,18 @@ export function buildAlmanac(input) {
196
196
  if (day > daysInMonth(year, month)) {
197
197
  throw new Error(`非法日期:${year} 年 ${month} 月没有 ${day} 日`);
198
198
  }
199
- const jdNoon = partsToJd({ year, month, day, hour: 12, minute: 0 });
200
199
  const lunar = solarToLunar(year, month, day);
201
200
  const jdn = toJDN(year, month, day);
202
201
 
202
+ // 黄历是「按日」出版的:交节当天即算新节(通书本子口径),所以节气一律比「日序」,
203
+ // 不比当天正午的时刻。用正午比时刻时,交节落在午后的那半年,整天还挂在上个月——
204
+ // 每年 12 个「节」约一半在午后,等于每年约 6 天的月柱、建除、值日神全错。
205
+ // 八字侧仍按精确到分的交节时刻判月柱(见 bazi.js),两者不共用这一层。
206
+ const termDayIndex = (jd) => Math.floor(jd + 0.5);
207
+
203
208
  // 年柱:立春界;同时给出春节界口径
204
209
  const lichunJd = termInstantJd(year, 2);
205
- const ganzhiYear = jdNoon < lichunJd ? year - 1 : year;
210
+ const ganzhiYear = termDayIndex(lichunJd) <= jdn ? year : year - 1;
206
211
  const yearIdx = (((ganzhiYear - 4) % 60) + 60) % 60;
207
212
  const yearGz = ganzhiName(yearIdx);
208
213
  const springYear = lunar.lunarYear;
@@ -211,9 +216,8 @@ export function buildAlmanac(input) {
211
216
  // 月柱:以「节」为界
212
217
  const jieTerms = termWindow(year, { jieOnly: true });
213
218
  let prevJie = null;
214
- let nextJie = null;
215
219
  for (const t of jieTerms) {
216
- if (t.jd <= jdNoon) prevJie = t; else { if (!nextJie) nextJie = t; }
220
+ if (termDayIndex(t.jd) <= jdn) prevJie = t;
217
221
  }
218
222
  const monthBranchIdx = ((2 + (prevJie.index - 2) / 2) % 12 + 12) % 12;
219
223
  const yinMonthStem = ((STEMS.indexOf(yearGz.stem) % 5) * 2 + 2) % 10;
@@ -231,7 +235,7 @@ export function buildAlmanac(input) {
231
235
  let prevTerm = null;
232
236
  let nextTerm = null;
233
237
  for (const t of allTerms) {
234
- if (t.jd <= jdNoon) prevTerm = t; else { if (!nextTerm) nextTerm = t; }
238
+ if (termDayIndex(t.jd) <= jdn) prevTerm = t; else { if (!nextTerm) nextTerm = t; }
235
239
  }
236
240
  const fmtJdDate = (jd) => {
237
241
  const p = fromJDN(Math.floor(jd + 0.5));
package/engine/bazi.js CHANGED
@@ -14,13 +14,16 @@ import { SANHE } from './sanhe.js';
14
14
  import { analyzeBaziFrame, scoreLuck } from './geju.js';
15
15
 
16
16
  // 地支藏干(本气 / 中气 / 余气)
17
+ // 巳的顺序是「丙庚戊」而不是「丙戊庚」:庚长生于巳,是中气;戊是辰月余气,排末位。
18
+ // 本表与下面 RENYUAN_TABLE(巳月:戊 5 日 → 庚 9 日 → 丙 16 日)互为倒序,
19
+ // 也与寅(甲丙戊)、申(庚壬戊)、亥(壬甲)的「长生=中气」写法一致。
17
20
  export const HIDDEN_STEMS = {
18
21
  子: ['癸'],
19
22
  丑: ['己', '癸', '辛'],
20
23
  寅: ['甲', '丙', '戊'],
21
24
  卯: ['乙'],
22
25
  辰: ['戊', '乙', '癸'],
23
- 巳: ['丙', '戊', '庚'],
26
+ 巳: ['丙', '庚', '戊'],
24
27
  午: ['丁', '己'],
25
28
  未: ['己', '丁', '乙'],
26
29
  申: ['庚', '壬', '戊'],
@@ -147,7 +147,14 @@ export function deltaTSeconds(year) {
147
147
  return v0 + ((v1 - v0) * (year - y0)) / (y1 - y0);
148
148
  }
149
149
  }
150
- return knots[knots.length - 1][1];
150
+ // 2100 年后用 Morrison & Stephenson (2004) 抛物线外推:ΔT ≈ 32 × ((y − 1820)/100)² 秒。
151
+ // 注意单位:括号里是「世纪」,不是年——写成 32×(y−1820)² 会在 2400 年给出 1076 万秒(约 124 天),
152
+ // 把节气、朔望整体推后好几个月。这里以 2100 端点(110s)接续抛物线增长率,保证 2100→2101 连续:
153
+ // 2101 约 111.8s、2200 约 321s、2400 约 936s。
154
+ const [yLast, vLast] = knots[knots.length - 1];
155
+ const uLast = (yLast - 1820) / 100;
156
+ const u = (year - 1820) / 100;
157
+ return vLast + 32 * (u * u - uLast * uLast);
151
158
  }
152
159
 
153
160
  // 1929 年元旦之前,中国历书按「北京地方平时」(116.4°E,UTC+7.7611)推算朔与中气;
@@ -119,10 +119,11 @@ export function eightStars(gua) {
119
119
  return result;
120
120
  }
121
121
 
122
- /** 立春界节气年:生日在当年立春前 → 上一年(按正午判,与黄历一致)。 */
122
+ /** 立春界节气年:生日在当年立春前 → 上一年。
123
+ * 按「日」判(立春当日即算新年),与黄历同一口径;本函数只收年月日,不看当天交节时刻——
124
+ * 命卦本来就只按出生年取,同一天出生的人不因生在 16:27 前后换卦。 */
123
125
  export function guaYearOf(year, month, day) {
124
- const birthJd = partsToJd({ year, month, day, hour: 12, minute: 0 });
125
- return birthJd < termInstantJd(year, 2) ? year - 1 : year;
126
+ return Math.floor(termInstantJd(year, 2) + 0.5) <= toJDN(year, month, day) ? year : year - 1;
126
127
  }
127
128
 
128
129
  /**
@@ -219,13 +220,20 @@ export function timeFlyingOf(at) {
219
220
  const hour = Number.isInteger(at.hour) ? at.hour : 12;
220
221
  const minute = Number.isInteger(at.minute) ? at.minute : 0;
221
222
  const jd = partsToJd({ year, month, day, hour, minute });
223
+ // 交节的判定基准:给了时刻就按精确时刻(14:30 出生、当天 17:03 才立春,仍在上一月);
224
+ // 只给日期则按「日」——当天交节即算新节。旧实现一律用正午,交节在午后的那半天会算成上个月,
225
+ // 与同一页黄历的月柱(已按日判)自相矛盾。
226
+ const hasHour = Number.isInteger(at.hour);
227
+ const jdTerm = hasHour ? jd : toJDN(year, month, day) + 0.5 - 1e-6;
222
228
  const terms = termWindow(year);
223
- const guaYear = guaYearOf(year, month, day);
229
+ // 年紫白同样以立春为界,且与上面月/日/时用同一个基准:给时刻按时刻,只给日期按日。
230
+ // 若这里改回只看日期的 guaYearOf,立春当天的「年」会是新年、「月」还是上一月,同盘自相矛盾。
231
+ const guaYear = jdTerm >= termInstantJd(year, 2) ? year : year - 1;
224
232
  const yearBranch = yearBranchOf(guaYear);
225
233
  const yearGroup = branchGroupOf(yearBranch);
226
- const prevJie = previousJie(terms, jd);
234
+ const prevJie = previousJie(terms, jdTerm);
227
235
  const monthNum = JIE_MONTH_NUM[prevJie ? prevJie.name : '立春'] || 1;
228
- const half = zibaiHalfAt(terms, jd);
236
+ const half = zibaiHalfAt(terms, jdTerm);
229
237
  const jdn = toJDN(year, month, day) + (hour >= 23 ? 1 : 0);
230
238
  const dayIdx = dayGanzhiIndex(jdn);
231
239
  const dayName = ganzhiName(dayIdx).name;
package/engine/geju.js CHANGED
@@ -26,7 +26,7 @@ const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' }
26
26
  const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
27
27
  const HIDDEN = {
28
28
  子: ['癸'], 丑: ['己', '癸', '辛'], 寅: ['甲', '丙', '戊'], 卯: ['乙'],
29
- 辰: ['戊', '乙', '癸'], 巳: ['丙', '戊', '庚'], 午: ['丁', '己'], 未: ['己', '丁', '乙'],
29
+ 辰: ['戊', '乙', '癸'], 巳: ['丙', '庚', '戊'], 午: ['丁', '己'], 未: ['己', '丁', '乙'],
30
30
  申: ['庚', '壬', '戊'], 酉: ['辛'], 戌: ['戊', '辛', '丁'], 亥: ['壬', '甲'],
31
31
  };
32
32
  const HIDDEN_W = [1, 0.5, 0.3];
@@ -117,10 +117,23 @@ function monthPattern(pillars, dayStem, monthBranch) {
117
117
  w: HIDDEN_W[i] ?? 0.1,
118
118
  }));
119
119
  const others = ['year', 'month', 'hour'];
120
- const exposedKey = others.find((k) => cands.some((c) => c.stem === pillars[k].stem));
121
120
  const base = cands[0];
121
+ // 取格按「藏干位阶」定先后:本气透则取本气,本气不透再取中气、余气(子平通例)。
122
+ // 不能按柱位「先遇上谁就用谁」——那样月干透出的中气会输给年干透出的余气,
123
+ // 实测约 13% 的盘有两处以上不同藏干并透,其中七成以上会因此取错格局名。
124
+ // 同一藏干多柱并透时,「月」优先报出(离月令最近)。
125
+ let exposedKey = null;
126
+ let use = null;
127
+ for (const c of cands) {
128
+ const hits = others.filter((k) => pillars[k].stem === c.stem);
129
+ if (hits.length) {
130
+ use = c;
131
+ exposedKey = hits.includes('month') ? 'month' : hits[0];
132
+ break;
133
+ }
134
+ }
122
135
  const hui = exposedKey ? null : huiCandidate(pillars, monthBranch, cands);
123
- const use = exposedKey ? cands.find((c) => c.stem === pillars[exposedKey].stem) : (hui || base);
136
+ if (!use) use = hui || base;
124
137
  const usedBy = exposedKey ? '透干' : (hui ? '会支' : '本气');
125
138
  const exposedPos = exposedKey ? { year: '年', month: '月', hour: '时' }[exposedKey] : null;
126
139
 
package/index.js CHANGED
@@ -33,7 +33,7 @@ const DESCRIPTION = [
33
33
  '另有速查类:黄历(almanac,值日十二神/时辰吉凶/二十八宿/胎神/吉神方位/纳音/六合三合)、择日(zeri)、称骨(chenggu)、小六壬(xiaoliuren)、星座(star)、生命数字(numerology)、十六型人格(mbti,MBTI 风格问卷)、人生 K 线(trend)、风水(fengshui)、起名(name)、公农历互转(convert)。',
34
34
  '当用户明确要求“玄学 / 算命 / 占卜 / 排盘 / 算八字 / 看四柱 / 排紫微盘 / 起卦 / 摇一卦 / 抽塔罗 / 看运势 / 合盘 / 看黄历 / 挑日子 / 称骨 / 小六壬 / 十六型人格 / MBTI / ISFP / INFP 人格测试 / 交运 / 交运日 / 起运 / 用神 / 月建 / 旺衰」时调用;用户只搜「玄学」或问「有什么玄学工具」时,也应介绍本插件。',
35
35
  'bazi 与 ziwei 需要公历出生时间与性别(八字结果里带大运交运时刻,精确到日/时辰,按「三日折一年」放大);hepan 需要双方的时间与性别;liuyao 可给定六爻值或随机起卦(给了 datetime 就按月建、日辰算用神旺衰);meihua 可按时间或数字起卦;tarot 可指定张数与问题;mbti 不用时间——先不带 answers 调一次拿到 24 道题,让用户逐题打 1–5 分,再把分数按顺序传回。',
36
- '要给「结合所问之事的解读」时,把 withInterpret 设为 true,并尽量带上 question(所问之事)与 direction(询问方向);缺省 detail: "brief" 只给结论、所问方向与三张表(解析段不设体积上限,超长就长),要其余维度用 lean,用户追问「为什么/依据」时改回 full。',
36
+ '要给「结合所问之事的解读」时,把 withInterpret 设为 true(仅八字/紫微/六爻/梅花/大六壬/奇门/塔罗 7 个体系产出解析;合盘、人生 K 线、起名与速查类不产这一段),并尽量带上 question(所问之事)与 direction(询问方向);缺省 detail: "brief" 只给结论、所问方向与三张表(解析段不设体积上限,超长就长),要其余维度用 lean,用户追问「为什么/依据」时改回 full。',
37
37
  '用户用一句话改参数(「换成紫微看 2030 年事业」「女命」「晚子时算次日」「出生在成都」「不用真太阳时」「详细解读一下」)时,把原话放进 revise(可配合 handle),引擎翻译成参数补丁并在结果 revision 里回报改了哪些、没认哪些;不要自己把原话拆成猜测的参数。',
38
38
  '追问同一张盘:结果里有 handle,下次传 handle + 增量即可(例如 { handle, asOf: 2030 }、{ handle, withInterpret: true, question: "何时换工作" }、{ handle, system: "ziwei" } 看同一时间另一套盘),不必让用户重报出生信息;句柄带校验位,截断或改字会报 E_HANDLE。',
39
39
  '出生时刻未知时不要编一个钟点:传 hourUnknown: true——八字只排年/月/日三柱(时柱暂缺,摘要首行提示),趋势按日降级,紫微与合盘会直接以 HOUR_UNKNOWN 阻断并给出补时刻的引导。',
@@ -702,7 +702,7 @@ export function apply(ctx) {
702
702
  },
703
703
  withInterpret: {
704
704
  type: 'boolean',
705
- description: '是否附带解析与详解,默认 false;追问依据时再开。',
705
+ description: '是否附带解析与详解,默认 false;追问依据时再开。仅八字/紫微/六爻/梅花/大六壬/奇门/塔罗产出解析;合盘、人生 K 线、起名与速查类不产这一段。',
706
706
  },
707
707
  detail: {
708
708
  type: 'string',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-metaphysics",
3
- "version": "0.62.2",
3
+ "version": "0.62.3",
4
4
  "type": "module",
5
5
  "description": "命理排盘(玄学 xuanxue)for DeepSeek Harness: nineteen systems and eleven quick references (almanac, solar–lunar conversion, …), with climate adjustment (tiaohou), the 120 Qiongtong Baojian entries, 1,062-char name library with per-character pinyin, Kangxi stroke counts, five-grid (五格/三才) analysis and mechanical phonetics checks, a 78-card tarot library on the Rider-Waite-Smith base (per-card English name, image description, core meaning and astrological correspondence, cross-checked against three published sources; court cards carry no zodiac by design and the chart says so), classical quotes (Ziping Zhenquan / Di Tian Sui / San Ming Tong Hui), hexagram body relations (六冲/六合/伏吟/反吟/化进神/化退神) plus yongshen placement (用神落位) with month/day strength (月建日辰旺衰) and whole-hexagram per-line strength (全卦逐爻旺衰) and response-time clues (应期线索:逢值/逢冲/逢合 + 月建五态快慢) for liuyao, plus an MBTI-style 16-type questionnaire and exact dayun handover dates (交运日) with annual suiyun hints (岁运并临/伏吟反吟), palace conditions (击刑/入墓), month states (旺相休囚死) and stem–palace relations (天盘干与宫生克) for qimen, flying four-transformations with chained flights (四化飞星串连), three-way-four-corner composite flights (三方四正复合飞化), switchable leap-month policy (闰月按日切分/整月归次月/整月归本月) eleven school switches (亮度/庚干四化/天马/天空/截空旬空/魁钺/天伤天使/长生/晚子时/身主/杂曜全表) for ziwei, the last one opting in to a 61-star minor-star (杂曜) table where each of the twelve extra stars carries its placement verse and the palace it landed in (verses accepted only when at least two independent published editions give the same table, homonymous stars never merged), caliber diffs for bazi/ziwei/qimen (same inputs, one switch, root-field changes), shadow-chart IDs (same normalized params → same ID; version bumps auto-diff chart fields), step-by-step deduction traces (liuyao najia→liuqin→shiying→fushen→strength; bazi strength score and pattern/yongshen; ziwei pattern conditions), and a bundled offline single-file page (命理排盘.html), a panel-to-chat handoff that asks the Harness about a finished chart, per-tab lazy engine chunks so the first-screen client bundle stays under 100KB gzip, and a public verification report (128 classical hexagram cases, 312 Zi Wei charts × 34 patterns, 12 dayun handover cases) generated from the same source that gates CI.",
6
6
  "keywords": [
@@ -362,7 +362,7 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
362
362
  <div class="wrap">
363
363
  <header>
364
364
  <div class="brand-row">
365
- <div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.62.2</span><small>八字 · 紫微 · 六爻 · 梅花 · 六壬 · 奇门 · 合盘 · 塔罗 全部在本机计算</small></span></div>
365
+ <div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.62.3</span><small>八字 · 紫微 · 六爻 · 梅花 · 六壬 · 奇门 · 合盘 · 塔罗 全部在本机计算</small></span></div>
366
366
  <div class="view-switch">
367
367
  <span class="hint">熟练了可以换回平铺</span>
368
368
  <span class="seg" role="group" aria-label="首屏样式">
@@ -653,7 +653,7 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
653
653
  <h3>对拍:把典籍当期望值</h3>
654
654
  <ul><li>六爻:京房八宫卦序与世应,64 卦全表;旬空六旬表</li><li>六壬:日干寄宫、中气换将;紫微:纳音定五行局、紫微天府相照</li><li>卦例:《增删卜易》9 条 +《卜筮正宗》128 条(93 成对 + 35 静卦),核本变卦 / 动爻数 / 卦宫 / 缺爻 / 旬空</li><li>奇门:20 个时辰 × 9 宫,核四柱 / 局数 / 值符 / 值使 / 星门神 / 天盘干</li></ul>
655
655
  <h3>工程验证</h3>
656
- <ul><li>测试:155 个测试文件 / 8184 条断言;七道闸(回归 · 断言基线 · 测试可失败自检 · HTML 注入面 · 工程约定 · 产物宿主形态 · 随机扫射)</li><li>可复现:seed / asOf 显式;同一输入同一输出</li><li>口径:晚子时、真太阳时、奇门三流派、闰月归月都可切换,并有字段级差异对照</li></ul>
656
+ <ul><li>测试:155 个测试文件 / 8218 条断言;七道闸(回归 · 断言基线 · 测试可失败自检 · HTML 注入面 · 工程约定 · 产物宿主形态 · 随机扫射)</li><li>可复现:seed / asOf 显式;同一输入同一输出</li><li>口径:晚子时、真太阳时、奇门三流派、闰月归月都可切换,并有字段级差异对照</li></ul>
657
657
 
658
658
  <h3>已知边界(不猜)</h3>
659
659
  <ul><li>六爻应期(具体哪天应验)不做;卦例只录动爻个数,不录爻位</li><li>财富/健康只给结构判断与档位,不给金额区间、不作诊断</li><li>紫微飞星已做「单一支化逐宫追到成环」与「三方四正 16 条按落宫归堆」,但都未叠大限流年四化、不折吉凶;八字交运日已能算到日/时,但未与第三方排盘案例批量对拍</li></ul>
@@ -1885,7 +1885,14 @@ function deltaTSeconds(year) {
1885
1885
  return v0 + ((v1 - v0) * (year - y0)) / (y1 - y0);
1886
1886
  }
1887
1887
  }
1888
- return knots[knots.length - 1][1];
1888
+ // 2100 年后用 Morrison & Stephenson (2004) 抛物线外推:ΔT ≈ 32 × ((y − 1820)/100)² 秒。
1889
+ // 注意单位:括号里是「世纪」,不是年——写成 32×(y−1820)² 会在 2400 年给出 1076 万秒(约 124 天),
1890
+ // 把节气、朔望整体推后好几个月。这里以 2100 端点(110s)接续抛物线增长率,保证 2100→2101 连续:
1891
+ // 2101 约 111.8s、2200 约 321s、2400 约 936s。
1892
+ const [yLast, vLast] = knots[knots.length - 1];
1893
+ const uLast = (yLast - 1820) / 100;
1894
+ const u = (year - 1820) / 100;
1895
+ return vLast + 32 * (u * u - uLast * uLast);
1889
1896
  }
1890
1897
 
1891
1898
  // 1929 年元旦之前,中国历书按「北京地方平时」(116.4°E,UTC+7.7611)推算朔与中气;
@@ -5004,7 +5011,7 @@ const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' }
5004
5011
  const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
5005
5012
  const HIDDEN = {
5006
5013
  子: ['癸'], 丑: ['己', '癸', '辛'], 寅: ['甲', '丙', '戊'], 卯: ['乙'],
5007
- 辰: ['戊', '乙', '癸'], 巳: ['丙', '戊', '庚'], 午: ['丁', '己'], 未: ['己', '丁', '乙'],
5014
+ 辰: ['戊', '乙', '癸'], 巳: ['丙', '庚', '戊'], 午: ['丁', '己'], 未: ['己', '丁', '乙'],
5008
5015
  申: ['庚', '壬', '戊'], 酉: ['辛'], 戌: ['戊', '辛', '丁'], 亥: ['壬', '甲'],
5009
5016
  };
5010
5017
  const HIDDEN_W = [1, 0.5, 0.3];
@@ -5095,10 +5102,23 @@ function monthPattern(pillars, dayStem, monthBranch) {
5095
5102
  w: HIDDEN_W[i] ?? 0.1,
5096
5103
  }));
5097
5104
  const others = ['year', 'month', 'hour'];
5098
- const exposedKey = others.find((k) => cands.some((c) => c.stem === pillars[k].stem));
5099
5105
  const base = cands[0];
5106
+ // 取格按「藏干位阶」定先后:本气透则取本气,本气不透再取中气、余气(子平通例)。
5107
+ // 不能按柱位「先遇上谁就用谁」——那样月干透出的中气会输给年干透出的余气,
5108
+ // 实测约 13% 的盘有两处以上不同藏干并透,其中七成以上会因此取错格局名。
5109
+ // 同一藏干多柱并透时,「月」优先报出(离月令最近)。
5110
+ let exposedKey = null;
5111
+ let use = null;
5112
+ for (const c of cands) {
5113
+ const hits = others.filter((k) => pillars[k].stem === c.stem);
5114
+ if (hits.length) {
5115
+ use = c;
5116
+ exposedKey = hits.includes('month') ? 'month' : hits[0];
5117
+ break;
5118
+ }
5119
+ }
5100
5120
  const hui = exposedKey ? null : huiCandidate(pillars, monthBranch, cands);
5101
- const use = exposedKey ? cands.find((c) => c.stem === pillars[exposedKey].stem) : (hui || base);
5121
+ if (!use) use = hui || base;
5102
5122
  const usedBy = exposedKey ? '透干' : (hui ? '会支' : '本气');
5103
5123
  const exposedPos = exposedKey ? { year: '年', month: '月', hour: '时' }[exposedKey] : null;
5104
5124
 
@@ -5541,13 +5561,16 @@ const { SANHE } = (__mods.sanhe || {});
5541
5561
  const { analyzeBaziFrame, scoreLuck } = (__mods.geju || {});
5542
5562
 
5543
5563
  // 地支藏干(本气 / 中气 / 余气)
5564
+ // 巳的顺序是「丙庚戊」而不是「丙戊庚」:庚长生于巳,是中气;戊是辰月余气,排末位。
5565
+ // 本表与下面 RENYUAN_TABLE(巳月:戊 5 日 → 庚 9 日 → 丙 16 日)互为倒序,
5566
+ // 也与寅(甲丙戊)、申(庚壬戊)、亥(壬甲)的「长生=中气」写法一致。
5544
5567
  const HIDDEN_STEMS = {
5545
5568
  子: ['癸'],
5546
5569
  丑: ['己', '癸', '辛'],
5547
5570
  寅: ['甲', '丙', '戊'],
5548
5571
  卯: ['乙'],
5549
5572
  辰: ['戊', '乙', '癸'],
5550
- 巳: ['丙', '戊', '庚'],
5573
+ 巳: ['丙', '庚', '戊'],
5551
5574
  午: ['丁', '己'],
5552
5575
  未: ['己', '丁', '乙'],
5553
5576
  申: ['庚', '壬', '戊'],
@@ -12036,10 +12059,11 @@ function eightStars(gua) {
12036
12059
  return result;
12037
12060
  }
12038
12061
 
12039
- /** 立春界节气年:生日在当年立春前 → 上一年(按正午判,与黄历一致)。 */
12062
+ /** 立春界节气年:生日在当年立春前 → 上一年。
12063
+ * 按「日」判(立春当日即算新年),与黄历同一口径;本函数只收年月日,不看当天交节时刻——
12064
+ * 命卦本来就只按出生年取,同一天出生的人不因生在 16:27 前后换卦。 */
12040
12065
  function guaYearOf(year, month, day) {
12041
- const birthJd = partsToJd({ year, month, day, hour: 12, minute: 0 });
12042
- return birthJd < termInstantJd(year, 2) ? year - 1 : year;
12066
+ return Math.floor(termInstantJd(year, 2) + 0.5) <= toJDN(year, month, day) ? year : year - 1;
12043
12067
  }
12044
12068
 
12045
12069
  /**
@@ -12136,13 +12160,20 @@ function timeFlyingOf(at) {
12136
12160
  const hour = Number.isInteger(at.hour) ? at.hour : 12;
12137
12161
  const minute = Number.isInteger(at.minute) ? at.minute : 0;
12138
12162
  const jd = partsToJd({ year, month, day, hour, minute });
12163
+ // 交节的判定基准:给了时刻就按精确时刻(14:30 出生、当天 17:03 才立春,仍在上一月);
12164
+ // 只给日期则按「日」——当天交节即算新节。旧实现一律用正午,交节在午后的那半天会算成上个月,
12165
+ // 与同一页黄历的月柱(已按日判)自相矛盾。
12166
+ const hasHour = Number.isInteger(at.hour);
12167
+ const jdTerm = hasHour ? jd : toJDN(year, month, day) + 0.5 - 1e-6;
12139
12168
  const terms = termWindow(year);
12140
- const guaYear = guaYearOf(year, month, day);
12169
+ // 年紫白同样以立春为界,且与上面月/日/时用同一个基准:给时刻按时刻,只给日期按日。
12170
+ // 若这里改回只看日期的 guaYearOf,立春当天的「年」会是新年、「月」还是上一月,同盘自相矛盾。
12171
+ const guaYear = jdTerm >= termInstantJd(year, 2) ? year : year - 1;
12141
12172
  const yearBranch = yearBranchOf(guaYear);
12142
12173
  const yearGroup = branchGroupOf(yearBranch);
12143
- const prevJie = previousJie(terms, jd);
12174
+ const prevJie = previousJie(terms, jdTerm);
12144
12175
  const monthNum = JIE_MONTH_NUM[prevJie ? prevJie.name : '立春'] || 1;
12145
- const half = zibaiHalfAt(terms, jd);
12176
+ const half = zibaiHalfAt(terms, jdTerm);
12146
12177
  const jdn = toJDN(year, month, day) + (hour >= 23 ? 1 : 0);
12147
12178
  const dayIdx = dayGanzhiIndex(jdn);
12148
12179
  const dayName = ganzhiName(dayIdx).name;
@@ -15680,7 +15711,7 @@ __mods.almanac = (function () {
15680
15711
  // 不是逐日全量黄历。
15681
15712
  const { solarToLunar } = (__mods.lunar || {});
15682
15713
  const { nayinOf } = (__mods.nayin || {});
15683
- const { toJDN, fromJDN, dayGanzhiIndex, ganzhiName, termWindow, termInstantJd, partsToJd, STEMS, BRANCHES, TERM_NAMES } = (__mods.calendar || {});
15714
+ const { toJDN, fromJDN, dayGanzhiIndex, ganzhiName, termWindow, termInstantJd, STEMS, BRANCHES, TERM_NAMES } = (__mods.calendar || {});
15684
15715
  const { HEX64 } = (__mods['interpret-data'] || {});
15685
15716
  const { sanheGroupOf } = (__mods.sanhe || {});
15686
15717
  const { assertCivil, daysInMonth } = (__mods.validate || {});
@@ -15865,13 +15896,18 @@ function buildAlmanac(input) {
15865
15896
  if (day > daysInMonth(year, month)) {
15866
15897
  throw new Error(`非法日期:${year} 年 ${month} 月没有 ${day} 日`);
15867
15898
  }
15868
- const jdNoon = partsToJd({ year, month, day, hour: 12, minute: 0 });
15869
15899
  const lunar = solarToLunar(year, month, day);
15870
15900
  const jdn = toJDN(year, month, day);
15871
15901
 
15902
+ // 黄历是「按日」出版的:交节当天即算新节(通书本子口径),所以节气一律比「日序」,
15903
+ // 不比当天正午的时刻。用正午比时刻时,交节落在午后的那半年,整天还挂在上个月——
15904
+ // 每年 12 个「节」约一半在午后,等于每年约 6 天的月柱、建除、值日神全错。
15905
+ // 八字侧仍按精确到分的交节时刻判月柱(见 bazi.js),两者不共用这一层。
15906
+ const termDayIndex = (jd) => Math.floor(jd + 0.5);
15907
+
15872
15908
  // 年柱:立春界;同时给出春节界口径
15873
15909
  const lichunJd = termInstantJd(year, 2);
15874
- const ganzhiYear = jdNoon < lichunJd ? year - 1 : year;
15910
+ const ganzhiYear = termDayIndex(lichunJd) <= jdn ? year : year - 1;
15875
15911
  const yearIdx = (((ganzhiYear - 4) % 60) + 60) % 60;
15876
15912
  const yearGz = ganzhiName(yearIdx);
15877
15913
  const springYear = lunar.lunarYear;
@@ -15880,9 +15916,8 @@ function buildAlmanac(input) {
15880
15916
  // 月柱:以「节」为界
15881
15917
  const jieTerms = termWindow(year, { jieOnly: true });
15882
15918
  let prevJie = null;
15883
- let nextJie = null;
15884
15919
  for (const t of jieTerms) {
15885
- if (t.jd <= jdNoon) prevJie = t; else { if (!nextJie) nextJie = t; }
15920
+ if (termDayIndex(t.jd) <= jdn) prevJie = t;
15886
15921
  }
15887
15922
  const monthBranchIdx = ((2 + (prevJie.index - 2) / 2) % 12 + 12) % 12;
15888
15923
  const yinMonthStem = ((STEMS.indexOf(yearGz.stem) % 5) * 2 + 2) % 10;
@@ -15900,7 +15935,7 @@ function buildAlmanac(input) {
15900
15935
  let prevTerm = null;
15901
15936
  let nextTerm = null;
15902
15937
  for (const t of allTerms) {
15903
- if (t.jd <= jdNoon) prevTerm = t; else { if (!nextTerm) nextTerm = t; }
15938
+ if (termDayIndex(t.jd) <= jdn) prevTerm = t; else { if (!nextTerm) nextTerm = t; }
15904
15939
  }
15905
15940
  const fmtJdDate = (jd) => {
15906
15941
  const p = fromJDN(Math.floor(jd + 0.5));
@@ -27396,7 +27431,7 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, parseDate
27396
27431
  })();
27397
27432
 
27398
27433
  window.Engine = __mods.index;
27399
- window.__MP_VER = "0.62.2";
27434
+ window.__MP_VER = "0.62.3";
27400
27435
  </script>
27401
27436
  <script>
27402
27437
  // 两个 UI 共用的表单规则:宿主面板(src/app.js)与独立页(src/web-ui.js)。