dsh-metaphysics 0.61.2 → 0.62.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +3 -3
  3. package/client.almanac.js +74 -69
  4. package/client.bazi.js +129 -124
  5. package/client.chenggu.js +75 -70
  6. package/client.convert.js +66 -61
  7. package/client.daliuren.js +129 -124
  8. package/client.fengshui.js +57 -52
  9. package/client.hepan.js +128 -123
  10. package/client.js +2 -2
  11. package/client.liuyao.js +127 -122
  12. package/client.mbti.js +66 -61
  13. package/client.name.js +73 -68
  14. package/client.numerology.js +72 -67
  15. package/client.qimen.js +125 -120
  16. package/client.star.js +72 -67
  17. package/client.tarot.js +123 -118
  18. package/client.trend.js +119 -114
  19. package/client.xiaoliuren.js +84 -79
  20. package/client.zeri.js +66 -61
  21. package/client.ziwei.js +129 -124
  22. package/engine/bazi-words.js +3 -0
  23. package/engine/bazi.js +29 -12
  24. package/engine/calendar.js +22 -1
  25. package/engine/chenggu.js +20 -6
  26. package/engine/convert.js +18 -9
  27. package/engine/daliuren.js +87 -18
  28. package/engine/datetime.js +73 -0
  29. package/engine/diff.js +1 -1
  30. package/engine/dst.js +0 -7
  31. package/engine/fengshui.js +19 -3
  32. package/engine/fingerprint.js +2 -1
  33. package/engine/followup.js +33 -3
  34. package/engine/geo.js +58 -12
  35. package/engine/handle.js +53 -1
  36. package/engine/index.js +135 -66
  37. package/engine/interpret-knowledge.js +3 -3
  38. package/engine/liuyao.js +27 -4
  39. package/engine/lunar.js +0 -7
  40. package/engine/mbti.js +1 -0
  41. package/engine/meihua.js +27 -16
  42. package/engine/name.js +15 -6
  43. package/engine/numerology.js +4 -3
  44. package/engine/qimen-interpret.js +28 -3
  45. package/engine/qimen-prompt.js +23 -17
  46. package/engine/qimen.js +32 -13
  47. package/engine/rectify.js +47 -6
  48. package/engine/star.js +5 -4
  49. package/engine/tarot.js +24 -6
  50. package/engine/tiaohou.js +17 -10
  51. package/engine/trend.js +14 -2
  52. package/engine/validate.js +30 -0
  53. package/engine/xiaoliuren.js +28 -7
  54. package/engine/yingqi.js +4 -9
  55. package/engine/zeri.js +19 -21
  56. package/engine/ziwei-hepan.js +3 -1
  57. package/engine/ziwei-words.js +2 -1
  58. package/engine/ziwei.js +15 -10
  59. package/index.js +99 -10
  60. package/package.json +1 -1
  61. package//345/221/275/347/220/206/346/216/222/347/233/230.html +11787 -11141
package/engine/geo.js CHANGED
@@ -7,7 +7,7 @@ const RAW = `
7
7
  广州 113.26 23.13;深圳 114.06 22.55;珠海 113.58 22.27;东莞 113.75 23.02;佛山 113.12 23.02
8
8
  汕头 116.68 23.35;中山 113.39 22.52;惠州 114.42 23.11;江门 113.08 22.58;湛江 110.36 21.27
9
9
  韶关 113.60 24.81;梅州 116.12 24.29;清远 113.05 23.70;肇庆 112.47 23.05;茂名 110.93 21.66
10
- 揭阳 116.37 23.55;潮州 116.62 23.66;汕尾 115.37 22.78;河源 114.70 23.74;阳江 111.98 21.86
10
+ 揭阳 116.37 23.55;潮州 116.62 23.66;汕尾 115.37 22.78;河源 114.70 23.74;阳江 111.98 21.86;云浮 112.05 22.92
11
11
  杭州 120.15 30.28;宁波 121.55 29.88;温州 120.70 28.00;嘉兴 120.75 30.75;湖州 120.09 30.89
12
12
  绍兴 120.58 30.05;金华 119.65 29.09;台州 121.42 28.66;舟山 122.10 30.00;衢州 118.87 28.94;丽水 119.92 28.45
13
13
  南京 118.80 32.06;苏州 120.62 31.32;无锡 120.31 31.49;常州 119.97 31.81;南通 120.89 31.98
@@ -16,7 +16,10 @@ const RAW = `
16
16
  合肥 117.23 31.82;芜湖 118.43 31.35;蚌埠 117.39 32.92;安庆 117.05 30.54;阜阳 115.81 32.90
17
17
  大同 113.30 40.08;阳泉 113.58 37.86;长治 113.12 36.19;晋城 112.85 35.49;朔州 112.43 39.33
18
18
  晋中 112.75 37.69;运城 111.00 35.02;忻州 112.73 38.42;临汾 111.52 36.09;吕梁 111.14 37.52
19
+ 唐山 118.18 39.63;秦皇岛 119.60 39.94;邯郸 114.49 36.61;邢台 114.50 37.07;保定 115.48 38.87
20
+ 张家口 114.89 40.82;承德 117.96 40.95;沧州 116.84 38.29;廊坊 116.68 39.54;衡水 115.67 37.74
19
21
  六安 116.50 31.75;马鞍山 118.50 31.67;淮南 117.00 32.63;滁州 118.32 32.30;黄山 118.34 29.71
22
+ 池州 117.49 30.66;宣城 118.75 30.95;铜陵 117.81 30.93;淮北 116.80 33.95;宿州 116.98 33.68;亳州 115.78 33.89
20
23
  福州 119.30 26.08;厦门 118.09 24.48;泉州 118.68 24.87;漳州 117.65 24.51;莆田 119.01 25.45
21
24
  宁德 119.55 26.67;三明 117.64 26.26;南平 118.18 26.64;龙岩 117.02 25.08
22
25
  南昌 115.86 28.68;赣州 114.93 25.83;九江 116.00 29.70;上饶 117.94 28.45;宜春 114.40 27.80
@@ -42,24 +45,25 @@ const RAW = `
42
45
  铜仁 109.19 27.72;凯里 107.98 26.58;都匀 107.52 26.26
43
46
  昆明 102.83 24.88;曲靖 103.80 25.49;玉溪 102.54 24.35;大理 100.23 25.60;丽江 100.23 26.86
44
47
  保山 99.17 25.11;昭通 103.72 27.34;普洱 100.97 22.79;临沧 100.09 23.88;景洪 100.80 22.00
45
- 拉萨 91.13 29.65;日喀则 88.88 29.27;林芝 94.36 29.65;昌都 97.17 31.14
48
+ 拉萨 91.13 29.65;日喀则 88.88 29.27;林芝 94.36 29.65;昌都 97.17 31.14;山南 91.77 29.23;那曲 92.05 31.48
46
49
  西安 108.94 34.34;咸阳 108.70 34.33;宝鸡 107.14 34.37;渭南 109.50 34.50;汉中 107.03 33.07
47
50
  榆林 109.73 38.29;延安 109.49 36.60;安康 109.03 32.68;商洛 109.94 33.87;铜川 108.95 34.90
48
51
  兰州 103.83 36.06;天水 105.72 34.58;白银 104.14 36.55;酒泉 98.49 39.73;张掖 100.45 38.93
49
52
  武威 102.63 37.93;庆阳 107.64 35.71;平凉 106.67 35.54;定西 104.63 35.58;陇南 104.93 33.40
53
+ 金昌 102.19 38.52;嘉峪关 98.29 39.79
50
54
  西宁 101.78 36.62;海东 102.10 36.50;格尔木 94.90 36.40
51
55
  银川 106.23 38.49;石嘴山 106.38 39.02;吴忠 106.20 37.99;固原 106.24 36.02
52
56
  乌鲁木齐 87.62 43.82;克拉玛依 84.89 45.58;喀什 75.99 39.47;伊宁 81.32 43.92;库尔勒 86.15 41.76
53
57
  阿克苏 80.26 41.17;哈密 93.51 42.83;吐鲁番 89.19 42.95;石河子 86.04 44.31
54
58
  呼和浩特 111.75 40.84;包头 109.84 40.66;鄂尔多斯 109.78 39.61;赤峰 118.89 42.26;通辽 122.24 43.65
55
- 呼伦贝尔 119.77 49.21;乌兰察布 113.13 40.99;巴彦淖尔 107.42 40.75
59
+ 呼伦贝尔 119.77 49.21;乌兰察布 113.13 40.99;巴彦淖尔 107.42 40.75;乌海 106.83 39.67
56
60
  沈阳 123.43 41.80;大连 121.62 38.92;鞍山 122.99 41.11;抚顺 123.96 41.88;本溪 123.77 41.30
57
61
  丹东 124.38 40.13;锦州 121.13 41.10;营口 122.24 40.67;阜新 121.67 42.02;辽阳 123.24 41.27
58
62
  盘锦 122.07 41.12;铁岭 123.84 42.29;朝阳 120.45 41.57;葫芦岛 120.84 40.71
59
63
  长春 125.32 43.82;吉林 126.55 43.84;四平 124.35 43.17;通化 125.94 41.73;白山 126.42 41.94
60
64
  松原 124.82 45.14;白城 122.84 45.62;延吉 129.51 42.91
61
65
  哈尔滨 126.53 45.80;齐齐哈尔 123.92 47.35;牡丹江 129.63 44.55;佳木斯 130.32 46.80;大庆 125.11 46.59
62
- 鸡西 130.97 45.30;双鸭山 131.16 46.65;伊春 128.84 47.73;鹤岗 130.30 47.35;黑河 127.53 50.24;绥化 126.97 46.65
66
+ 鸡西 130.97 45.30;双鸭山 131.16 46.65;伊春 128.84 47.73;鹤岗 130.30 47.35;黑河 127.53 50.24;绥化 126.97 46.65;七台河 131.00 45.77
63
67
  南宁 108.37 22.82;柳州 109.42 24.33;桂林 110.29 25.27;梧州 111.28 23.48;北海 109.12 21.48
64
68
  玉林 110.16 22.63;百色 106.62 23.90;河池 108.06 24.70;钦州 108.65 21.98;防城港 108.35 21.69
65
69
  贵港 109.60 23.11;贺州 111.57 24.40;来宾 109.23 23.75;崇左 107.36 22.40
@@ -103,7 +107,19 @@ const CITIES = (() => {
103
107
  return map;
104
108
  })();
105
109
 
106
- const SUFFIX = /(省|市|地区|盟|自治州|自治县|自治区|特别行政区|县|区|镇|乡|街道|村|小区|社区)/g;
110
+ /**
111
+ * 城市名紧跟着这些字时,说的不是那座城市本身(「中山路」「中山区」「吉林省」)。
112
+ * 「县」不在表里:县名多与辖它的市同名(长沙县、乌鲁木齐县、承德县),县城就在城区边上,
113
+ * 按本市经度算比判成「未匹配」准得多——未匹配会静默取消真太阳时。
114
+ * 而「镇/乡/村」仍要挡:重庆开州区有个长沙镇,按湖南长沙算就错出两千公里。
115
+ */
116
+ const NOT_CITY_AFTER = /[省区路街道巷弄号村镇乡组队]/;
117
+
118
+ // 与地级市同名的区:写「中山区」指的是大连中山区,不能按广东中山市算经度。
119
+ const HOMONYM_DISTRICTS = {
120
+ 中山区: { city: '大连' },
121
+ 朝阳区: { city: '北京', byProvince: { 吉林: '长春', 辽宁: '朝阳' } },
122
+ };
107
123
 
108
124
  /** 地址 -> { lon, lat, matched, level, note } */
109
125
  export function resolveAddress(input) {
@@ -116,17 +132,47 @@ export function resolveAddress(input) {
116
132
  return { lon: num, lat: null, matched: `${num}°E`, level: '经度', note: '按你输入的经度计算' };
117
133
  }
118
134
 
119
- const text = raw.replace(/\s/g, '').replace(SUFFIX, '');
135
+ const rawFlat = raw.replace(/\s/g, '');
120
136
  const names = Object.keys(CITIES).sort((a, b) => b.length - a.length);
121
137
 
138
+ // 只认「城市本身」:带「市」的优先,否则取位置靠前的(地址通常先市后区/路)
139
+ const hits = [];
122
140
  for (const n of names) {
123
- if (text.includes(n)) {
124
- const c = CITIES[n];
125
- return { lon: c.lon, lat: c.lat, matched: n, level: '地级市', note: `按 ${n} 估算(约 ${c.lon}°E)` };
141
+ for (let at = rawFlat.indexOf(n); at >= 0; at = rawFlat.indexOf(n, at + n.length)) {
142
+ const after = rawFlat.charAt(at + n.length);
143
+ if (NOT_CITY_AFTER.test(after)) continue;
144
+ hits.push({ name: n, at, after, explicit: after === '市' ? 1 : 0 });
145
+ }
146
+ }
147
+ hits.sort((a, b) => b.explicit - a.explicit || a.at - b.at);
148
+ if (hits.length) {
149
+ const hit = hits[0];
150
+ const c = CITIES[hit.name];
151
+ // 「长沙县」不能回成「地级市 长沙」:经度借用的是城区,档位要说清是按县所在辖区估的
152
+ if (hit.after === '县') {
153
+ return { lon: c.lon, lat: c.lat, matched: `${hit.name}县`, level: '区县', note: `「${hit.name}县」按 ${hit.name} 城区估算(约 ${c.lon}°E)` };
126
154
  }
155
+ return { lon: c.lon, lat: c.lat, matched: hit.name, level: '地级市', note: `按 ${hit.name} 估算(约 ${c.lon}°E)` };
156
+ }
157
+
158
+ // 同名区县:先用原文里的省份上下文,再退回默认城市
159
+ for (const [district, rule] of Object.entries(HOMONYM_DISTRICTS)) {
160
+ if (!rawFlat.includes(district)) continue;
161
+ let city = rule.city;
162
+ for (const [prov, alt] of Object.entries(rule.byProvince || {})) {
163
+ if (rawFlat.includes(prov)) { city = alt; break; }
164
+ }
165
+ const c = CITIES[city];
166
+ if (c) {
167
+ return { lon: c.lon, lat: c.lat, matched: city, level: '区县', note: `「${district}」按 ${city} 估算(约 ${c.lon}°E)` };
168
+ }
169
+ }
170
+ // 只有「中山路」这种同名路名、没有城市线索时,宁可不猜,也不套用同名城市
171
+ const road = names.find((n) => rawFlat.includes(`${n}路`) || rawFlat.includes(`${n}街`));
172
+ if (road) {
173
+ return { lon: null, lat: null, matched: null, level: '未匹配', note: `「${road}路」是全国常见的路名,无法确定城市;请补上城市(如「厦门市${road}路」)或直接填经度` };
127
174
  }
128
175
  // 省名判定用未去后缀的原文,否则「河北区」会被去掉「区」后误判成河北省
129
- const rawFlat = raw.replace(/\s/g, '');
130
176
  for (const [prov, capital] of Object.entries(PROVINCES)) {
131
177
  const at = rawFlat.indexOf(prov);
132
178
  if (at < 0) continue;
@@ -142,8 +188,8 @@ export function resolveAddress(input) {
142
188
  const DETAIL = /(区|县|镇|乡|村|小区|街道|路|号|巷|弄|组|队)/;
143
189
 
144
190
  /** OpenStreetMap Nominatim:免密钥、支持中文。失败返回 null。
145
- * UA 按 OSM 使用政策要求写明应用与联系方式(改版本时同步这里)。 */
146
- const USER_AGENT = 'dsh-metaphysics/0.23.0 (+https://github.com/xuanfu-web/dsh-metaphysics)';
191
+ * OSM 使用政策只要求能识别应用与联系方式;不写版本号,避免发版时漏改造成失联。 */
192
+ const USER_AGENT = 'dsh-metaphysics (+https://github.com/xuanfu-web/dsh-metaphysics)';
147
193
 
148
194
  async function onlineLookup(text, { timeoutMs = 2500 } = {}) {
149
195
  if (typeof fetch !== 'function') return null;
package/engine/handle.js CHANGED
@@ -25,6 +25,8 @@ const PREFIX = 'mc1.';
25
25
  const SKIP = new Set([
26
26
  'handle', 'trace', 'shadow', 'shadowVersion', 'diff', 'withInterpret', 'detail', 'cross',
27
27
  'verifyPast', 'rectify', 'events', 'ask', 'followup', 'question', 'geocode', 'caliber',
28
+ // 联网解析的详细 geo 会随 display_name 变化,同一张盘却换来不同句柄;只留 longitude。
29
+ 'geo', 'longitudeFromGeo',
28
30
  // 一句话改参是把原话翻成补丁:补丁已经落在其余参数里,原话本身不进句柄,
29
31
  // 否则同一张盘换个说法就换一个句柄,回放时还会再套一遍补丁。
30
32
  'revise',
@@ -171,9 +173,58 @@ export function decodeHandle(handle) {
171
173
  return payload;
172
174
  }
173
175
 
174
- /** 不能作为增量键的名字:`out['__proto__'] = v` 走的是 setter,会把 out 的原型换掉 */
176
+ /** 不能作为增量键的名字:`out['__proto__'] = v` 走的是 setter,会把原型换掉 */
175
177
  const KEY_BLOCKED = new Set(['__proto__', 'constructor', 'prototype']);
176
178
 
179
+ /**
180
+ * 含义由「哪个体系」决定的键:值本身不说明是哪套盘的输入。
181
+ * 只列真正会坏事的那些——要么被工具层的入参守卫拦住(hour / date / mode),
182
+ * 要么换到的体系恰好也读同一个键名(minute 与 hour 同属 name;action 被 fengshui 与 name 共用)。
183
+ * 其余单体系键(num1–3 / upper / lines / count / 生肖…)只有原体系会读,留着也只是被忽略。
184
+ */
185
+ const SYSTEM_KEYS = {
186
+ hour: ['chenggu', 'xiaoliuren', 'name'],
187
+ minute: ['name'],
188
+ date: ['xiaoliuren'],
189
+ mode: ['convert', 'meihua', 'xiaoliuren'],
190
+ action: ['fengshui', 'name'],
191
+ };
192
+
193
+ /**
194
+ * 换体系时,把句柄带进来的、新体系不认的输入键剔掉。
195
+ *
196
+ * 为什么:文档主推 `{ handle, system: "ziwei" }`(同一时间换一套盘看),而称骨 / 小六壬的句柄里
197
+ * 一定带着 `hour`,convert / 梅花 / 小六壬的句柄一定带着 `mode`。这些键并回参数后,工具层的
198
+ * 「hour 仅用于 chenggu / xiaoliuren」守卫会把它们当成用户本轮误传而直接报错——上一张盘的输入
199
+ * 挡住了这一张盘。本次显式重给的键不动:那确实是用户这次的意思,该报错仍然报错。
200
+ *
201
+ * @param {object} merged 已并好句柄与增量的参数(原地修改)
202
+ * @param {Set<string>} owned 本轮由调用方显式给出的键
203
+ */
204
+ export function pruneForeignKeys(merged, owned) {
205
+ if (!isObject(merged)) return merged;
206
+ const system = merged.system;
207
+ if (typeof system !== 'string' || !system) return merged;
208
+ for (const [key, owners] of Object.entries(SYSTEM_KEYS)) {
209
+ if (owned.has(key) || merged[key] === undefined) continue;
210
+ if (owners.includes(system)) continue;
211
+ delete merged[key];
212
+ }
213
+ return merged;
214
+ }
215
+
216
+ /** 调用方本轮显式给出的键(undefined 视同没给,与 resolveHandle 的合并口径一致) */
217
+ export function ownedKeys(...sources) {
218
+ const out = new Set();
219
+ for (const src of sources) {
220
+ if (!isObject(src)) continue;
221
+ for (const [k, v] of Object.entries(src)) {
222
+ if (v !== undefined && k !== 'handle') out.add(k);
223
+ }
224
+ }
225
+ return out;
226
+ }
227
+
177
228
  /**
178
229
  * 把 handle 与增量参数并起来。没有 handle 时原样返回(深一层的浅拷贝,避免改调用方对象)。
179
230
  * 增量优先:{ handle, asOf: 2030 } 只覆盖 asOf,其余沿用原参数。
@@ -189,6 +240,7 @@ export function resolveHandle(args = {}) {
189
240
  if (v === undefined || KEY_BLOCKED.has(k)) continue;
190
241
  out[k] = v;
191
242
  }
243
+ pruneForeignKeys(out, ownedKeys(rest));
192
244
  return out;
193
245
  }
194
246
 
package/engine/index.js CHANGED
@@ -11,7 +11,7 @@ import { buildQimen, buildSnapshot } from './qimen.js';
11
11
  import { geocodeAddress, resolveAddress } from './geo.js';
12
12
  import { toJDN, ganzhiName, dayGanzhiIndex } from './calendar.js';
13
13
  import { randomOf, isSeed } from './random.js';
14
- import { encodeHandle, decodeHandle, resolveHandle } from './handle.js';
14
+ import { encodeHandle, decodeHandle, resolveHandle, pruneForeignKeys, ownedKeys } from './handle.js';
15
15
  import { askLink, parseAskLink, formOf, restNote, askPackages } from './bridge.js';
16
16
  import { parseRevision, displayValue } from './revise.js';
17
17
  import { interpret, DIRECTIONS, DIRECTION_DEFAULT } from './interpret.js';
@@ -36,21 +36,15 @@ import { buildName } from './name.js';
36
36
  import { buildTrace, traceMarkdown, SYSTEM_LABEL } from './trace.js';
37
37
  import { fail } from './fail.js';
38
38
  import { assertCivil } from './validate.js';
39
+ import { parseDateTime, daysInMonth } from './datetime.js';
39
40
 
40
41
  export { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf };
42
+ export { parseDateTime };
41
43
 
42
44
  const pad = (n) => String(n).padStart(2, '0');
43
45
 
44
46
  const GENDERS = ['male', 'female'];
45
47
 
46
- function daysInMonth(year, month) {
47
- if (month === 2) {
48
- const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
49
- return leap ? 29 : 28;
50
- }
51
- return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
52
- }
53
-
54
48
  /**
55
49
  * 性别是必填项:它决定大运/大限的顺逆,缺省按某一方排等于给出一张错盘。
56
50
  * 之前用 `=== 'female' ? 'female' : 'male'` 兜底,静默把未填性别的人按男命排。
@@ -77,41 +71,6 @@ function ziweiSchoolArgs(args) {
77
71
  return out;
78
72
  }
79
73
 
80
- /** 解析 'YYYY-MM-DDTHH:mm' / 'YYYY-MM-DD HH:mm' / 'YYYY-MM-DD'
81
- * 可以带秒(用户常直接粘 ISO 串),但引擎的民用时间口径到分钟:秒只校验、不参与换算。 */
82
- export function parseDateTime(text) {
83
- if (typeof text !== 'string' || !text.trim()) {
84
- throw fail('E_INPUT', '缺少 datetime(公历日期时间),例如 1990-05-12T14:30');
85
- }
86
- const m = text.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/);
87
- if (!m) throw fail('E_INPUT', `无法解析 datetime:「${text}」,请用 1990-05-12T14:30 这种格式`);
88
- // 秒位以前只捕获不校验:\d{1,2} 放过 0–99,于是「T03:04:99」被静默折成 03:04。
89
- // 与 1990-02-30 同一类问题——非法输入悄悄变成一个合法时刻,盘面照样排出,没人知道。
90
- if (m[6] !== undefined && Number(m[6]) > 59) throw fail('E_INPUT', `秒超出范围:${m[6]}`);
91
- const out = {
92
- year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
93
- hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0),
94
- };
95
- if (out.month < 1 || out.month > 12) throw fail('E_INPUT', `月份超出范围:${out.month}`);
96
- // 只校验 1..31 会让 1990-02-30、2026-04-31 这类日期静默滚到下个月,整盘错位且无人察觉
97
- const maxDay = daysInMonth(out.year, out.month);
98
- if (out.day < 1 || out.day > maxDay) {
99
- throw fail('E_INPUT', `日期不存在:${out.year}-${out.month}-${out.day}(${out.year} 年 ${out.month} 月只有 ${maxDay} 天)`);
100
- }
101
- if (out.hour > 23 || out.minute > 59) throw fail('E_INPUT', `时间超出范围:${out.hour}:${out.minute}`);
102
- // ΔT(地球自转改正)表只覆盖 1900–2100,超出后按端点值外推,精度不可保证
103
- if (out.year < 1600 || out.year > 2400) {
104
- throw fail('E_RANGE', `年份 ${out.year} 超出可计算范围(1600–2400)`);
105
- }
106
- if (out.year < 1900 || out.year > 2100) {
107
- out.precision = {
108
- level: 'approx',
109
- note: `ΔT 模型仅覆盖 1900–2100,${out.year} 年按端点值外推,节气与朔望时刻可能有分钟级偏差,月柱在交节前后需复核`,
110
- };
111
- }
112
- return out;
113
- }
114
-
115
74
  function baziText(r) {
116
75
  const P = r.pillars;
117
76
  const line = (label, p) =>
@@ -528,7 +487,7 @@ function jsonSafe(value) {
528
487
  // 只收影响盘面的参数:地址换成有效经度后不再把原文算进去,问事/方向按体系保留。
529
488
  const SHADOW_IGNORE = new Set([
530
489
  'shadow', 'shadowVersion', 'diff', 'trace', 'traceError', 'withInterpret', 'detail', 'cross', 'verifyPast', 'rectify', 'events',
531
- 'geocode', 'ask', 'followup', 'caliber', 'seed', 'asOf', 'handle', 'revise',
490
+ 'geocode', 'ask', 'followup', 'caliber', 'seed', 'asOf', 'handle', 'revise', 'geo', 'longitudeFromGeo',
532
491
  ]);
533
492
 
534
493
  const num6 = (n) => {
@@ -609,15 +568,20 @@ function shadowParams(args, result) {
609
568
 
610
569
  // 其余体系:只剔除解析/附加模块参数,其余原样归一化
611
570
  const params = {};
571
+ // 一律 defineProperty,不用 params[key] =:key 恰好是 __proto__ 时赋值走原型 setter,
572
+ // 键会静默消失——两种不同的 __proto__ 值于是撞成同一个指纹(canonicalJson 已避过这个坑)
573
+ const put = (key, value) => Object.defineProperty(params, key, {
574
+ value, enumerable: true, writable: true, configurable: true,
575
+ });
612
576
  for (const [key, value] of Object.entries(args)) {
613
577
  if (SHADOW_IGNORE.has(key) || value === undefined) continue;
614
- if (key === 'datetime') params.datetime = shadowDateTime(value);
615
- else if (key === 'partner') params.partner = shadowPerson(value);
616
- else if (key === 'gender') params.gender = String(value).toLowerCase();
578
+ if (key === 'datetime') put(key, shadowDateTime(value));
579
+ else if (key === 'partner') put(key, shadowPerson(value));
580
+ else if (key === 'gender') put(key, String(value).toLowerCase());
617
581
  else if (key === 'address') continue; // 调用侧先把地址换成经度,再由 longitude 参与;这里不收原文
618
- else if (typeof value === 'number') params[key] = Number.isFinite(value) ? (Object.is(value, -0) ? 0 : value) : null;
619
- else if (typeof value === 'string') params[key] = value.trim();
620
- else params[key] = value;
582
+ else if (typeof value === 'number') put(key, Number.isFinite(value) ? (Object.is(value, -0) ? 0 : value) : null);
583
+ else if (typeof value === 'string') put(key, value.trim());
584
+ else put(key, value);
621
585
  }
622
586
  if (system === 'daliuren' && params.lateZi === undefined) params.lateZi = true;
623
587
  if (system === 'tarot' && params.count === undefined) params.count = 3;
@@ -636,7 +600,7 @@ function shadowParams(args, result) {
636
600
  return params;
637
601
  }
638
602
 
639
- function shadowMeta(system, args) {
603
+ function shadowMeta(system, args, asOfNorm = normalizeAsOf(args.asOf)) {
640
604
  const seed = args.seed;
641
605
  const seeded = !(seed === undefined || seed === null || seed === '');
642
606
  // 六爻给定了六爻值就是确定性起卦,不需要 seed 也能复算
@@ -647,11 +611,28 @@ function shadowMeta(system, args) {
647
611
  return { reproducible: false, reason: '未给 seed:重算会变成另一副牌 / 另一卦' };
648
612
  }
649
613
  if (system === 'bazi' || system === 'ziwei' || system === 'trend' || system === 'fengshui') {
650
- return { reproducible: true, asOfPinned: !!args.asOf };
614
+ // 以归一化后的值为准:'2030-01-01' 算钉住了,' ' 不算
615
+ return { reproducible: true, asOfPinned: !!asOfNorm };
651
616
  }
652
617
  return { reproducible: true };
653
618
  }
654
619
 
620
+ /** asOf 也按同一套日期尺归一:2026-10-22 / 2026-10-22T16:00 / 带空格 三种写法应是同一张盘。
621
+ * 必须走 asOfParts,不能走 parseDateTime:排盘侧「只给日期」按中午 12:00 补时刻,
622
+ * parseDateTime 补的是 00:00——以前指纹用后者,于是 '2030-01-01' 与 '2030-01-01T12:00'
623
+ * 出的盘逐字段相同,baseId 却是两个,跨版本对拍直接断在这里。
624
+ * checkAsOf 与影子盘指纹共用这一个口径。 */
625
+ function normalizeAsOf(value) {
626
+ if (value === undefined || value === null || value === '') return null;
627
+ if (typeof value === 'object') return value; // 内部传的对象口径不动,由调用方负责
628
+ const s = String(value).trim();
629
+ if (!s) return null; // 只有空格 = 没钉 asOf,别让它算成第三种指纹
630
+ const parts = asOfParts(s);
631
+ if (!parts) return s; // 非法值留给 checkAsOf 报错,指纹不抢答
632
+ const p2 = (n) => String(n).padStart(2, '0');
633
+ return `${parts.year}-${p2(parts.month)}-${p2(parts.day)}T${p2(parts.hour)}:${p2(parts.minute)}`;
634
+ }
635
+
655
636
  /**
656
637
  * 影子盘指纹。版本优先级:显式 opts.version → 构建注入的全局 __MP_VER → dev。
657
638
  * @returns {{id:string, baseId:string, version:string, system:string, payload:string,
@@ -661,14 +642,15 @@ export function chartFingerprint(args = {}, opts = {}) {
661
642
  const result = opts.result || null;
662
643
  const system = String((result && result.system) || args.system || '');
663
644
  const version = String(opts.version || (typeof globalThis !== 'undefined' && globalThis.__MP_VER) || 'dev');
645
+ const asOf = normalizeAsOf(args.asOf);
664
646
  const fp = fingerprintOf({
665
647
  system,
666
648
  params: shadowParams(args, result),
667
649
  seed: args.seed,
668
- asOf: args.asOf ? String(args.asOf) : null,
650
+ asOf,
669
651
  version,
670
652
  });
671
- return { ...fp, ...shadowMeta(system, args), system, version };
653
+ return { ...fp, ...shadowMeta(system, args, asOf), system, version };
672
654
  }
673
655
 
674
656
  const SHADOW_DIFF_IGNORE = new Set([
@@ -760,6 +742,21 @@ export function compute(input = {}) {
760
742
  }),
761
743
  ignored: rev.ignored,
762
744
  };
745
+ // 换体系有两条路:显式 `system`(resolveHandle 里已经剔过)、以及一句话改参
746
+ // (「换成八字看」把 system 翻成补丁,落在句柄合并之后)。后一条若不补这一刀,
747
+ // 称骨句柄里的 hour 会跟到八字盘上,被工具层当成误传报错。
748
+ args = pruneForeignKeys(args, ownedKeys(input, rev.patch));
749
+ }
750
+ // asOf 在入口就归一成 canonical 串(去首尾空格、只给日期补 12:00):
751
+ // 排盘、句柄、影子盘指纹读同一份。以前只有指纹 trim,句柄照抄原文,
752
+ // '2030-01-01' 与 '2030-01-01T12:00' 出的明明是同一张盘,却各得一个 handle、一个 baseId。
753
+ // 没给 asOf 时一个键都不加:null 会整段进句柄,等于给所有旧深链换一次身份。
754
+ if (args.asOf !== undefined && args.asOf !== null && args.asOf !== '') {
755
+ const norm = normalizeAsOf(args.asOf);
756
+ if (norm === null) {
757
+ args = { ...args };
758
+ delete args.asOf; // 只有空格:按「没钉 asOf」处理,与 normalizeAsOf 的口径一致
759
+ } else if (norm !== args.asOf) args = { ...args, asOf: norm };
763
760
  }
764
761
  // partner2: null 视同未传:先归一化再排盘,否则同一对盘会因为显式 null 多出一个句柄
765
762
  if (args.partner2 === null) {
@@ -768,6 +765,15 @@ export function compute(input = {}) {
768
765
  }
769
766
  // 经度整形 + 守卫(非有限 / 超范围 → E_RANGE;数字串 → 数字)
770
767
  args = normalizeLongitudes(args);
768
+ // verifyPast 必须在「用它摊开过去流年」之前整形:这段逻辑在 compute() 里,
769
+ // 校验却留在 computeRaw(),数字串 "6" 会先被当成没传、再也没机会排。
770
+ if (args.verifyPast !== undefined && args.verifyPast !== null && args.verifyPast !== '') {
771
+ const n = Number(args.verifyPast);
772
+ if (!Number.isInteger(n) || n < 1 || n > 10) {
773
+ throw fail('E_INPUT', `verifyPast 要是 1–10 的整数(年数),收到「${args.verifyPast}」`);
774
+ }
775
+ args = { ...args, verifyPast: n };
776
+ }
771
777
  const r = jsonSafe(computeRaw(args));
772
778
  // 「所问之事」以前只有六爻和塔罗会落到结果里,梅花/六壬/奇门/小六壬都丢掉了:
773
779
  // 于是第一屏想说「你问的是……」却拿不到那句话。这里统一补齐,已有的值不动。
@@ -852,10 +858,13 @@ ${r.summary}`;
852
858
  }
853
859
  // 验前事:把过去若干年的流年信号摊开(只读,不改盘)
854
860
  if (Number.isInteger(args.verifyPast) && args.verifyPast > 0 && r.system === 'bazi') {
855
- const years = Math.min(Math.max(args.verifyPast, 1), 10);
861
+ const years = args.verifyPast;
856
862
  const from = Number(r.liunianFrom) - years;
857
863
  const pastChart = buildBazi({
858
864
  ...parseDateTime(args.datetime),
865
+ // 与主盘同口径:主盘 hourUnknown 时内部按正午占位,过去盘若还用 00:00,
866
+ // 流年信号就来自子夜的盘,与用户看到的不是同一张
867
+ hourUnknown: args.hourUnknown === true,
859
868
  gender: requireGender(args.gender),
860
869
  lateZi: args.lateZi !== false,
861
870
  trueSolar: args.trueSolar !== false,
@@ -1058,8 +1067,20 @@ const LATE_ZI_MODES = ['same-day', 'next-day'];
1058
1067
  */
1059
1068
  function checkLongitude(value, who = '') {
1060
1069
  if (value === undefined || value === null || value === '') return undefined;
1061
- const n = Number(value);
1062
1070
  const tag = who ? `${who}的` : '';
1071
+ // 只收 number 与十进制数字串。Number(true)=1、Number([])=0、Number(' ')=0 都是
1072
+ // 「看起来能转成数」的静默错值,按 1°/0° 做真太阳时还会被句柄固化。
1073
+ let n;
1074
+ if (typeof value === 'number') n = value;
1075
+ else if (typeof value === 'string') {
1076
+ const s = value.trim();
1077
+ if (!/^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(s)) {
1078
+ throw fail('E_RANGE', `${tag}经度不是十进制数字:收到「${value}」。经度写数字即可,例如北京 116.4`);
1079
+ }
1080
+ n = Number(s);
1081
+ } else {
1082
+ throw fail('E_RANGE', `${tag}经度必须是数字或数字字符串,收到 ${Array.isArray(value) ? 'array' : typeof value}`);
1083
+ }
1063
1084
  if (!Number.isFinite(n)) throw fail('E_RANGE', `${tag}经度不是有限数值:收到「${value}」。经度写十进制数字即可,例如北京 116.4`);
1064
1085
  if (n < -180 || n > 180) throw fail('E_RANGE', `${tag}经度要在 −180 到 180 之间(东经为正),收到 ${n}`);
1065
1086
  return n;
@@ -1069,8 +1090,9 @@ function checkLongitude(value, who = '') {
1069
1090
  * 把主方 / 对方 / 第三方的经度统一成数字。
1070
1091
  * 下游按 `typeof === 'number'` 取值,所以 "86.0"(模型常给带引号的数)会被静默丢掉,
1071
1092
  * 少一次真太阳时修正而结果里毫无痕迹——这里先整形,再交给校验过的值。
1093
+ * 宿主层也调它:地址解析前不归一,数字串形式的显式经度会被联网结果顶掉。
1072
1094
  */
1073
- function normalizeLongitudes(args) {
1095
+ export function normalizeLongitudes(args) {
1074
1096
  const self = checkLongitude(args.longitude, '');
1075
1097
  const p = args.partner && typeof args.partner === 'object' ? checkLongitude(args.partner.longitude, '对方') : undefined;
1076
1098
  const p2 = args.partner2 && typeof args.partner2 === 'object' ? checkLongitude(args.partner2.longitude, '第三方') : undefined;
@@ -1095,7 +1117,9 @@ function detailOf(value) {
1095
1117
 
1096
1118
  /** asOf 只接受能解析成真实日期的字符串;非法值以前会静默回退到「此刻」 */
1097
1119
  function checkAsOf(asOf) {
1098
- if (asOf === undefined || asOf === null || asOf === '') return undefined;
1120
+ // 空格串按「没给」处理,与影子盘指纹的 normalizeAsOf 同一条线:
1121
+ // 否则一边算成没钉 asOf、另一边报 E_ASOF
1122
+ if (asOf === undefined || asOf === null || (typeof asOf === 'string' && !asOf.trim())) return undefined;
1099
1123
  const parts = asOfParts(asOf);
1100
1124
  if (!parts) throw fail('E_ASOF', `无法解析 asOf:「${asOf}」,请用 2026-10-22T16:00 这种格式`);
1101
1125
  const { year, month, day, hour, minute } = parts;
@@ -1112,6 +1136,30 @@ function checkAsOf(asOf) {
1112
1136
 
1113
1137
  function computeRaw(args = {}) {
1114
1138
  const system = args.system;
1139
+ // mode 是三个体系共用的键,以前传错组的值会被静默忽略(convert 收到 'number' 仍按公历转农历)。
1140
+ // 这里按 system 分组校验,跨组直接报错,让模型改参数而不是拿到一张 silently wrong 的盘。
1141
+ if (args.mode !== undefined && args.mode !== null && args.mode !== '') {
1142
+ const MODE_BY_SYSTEM = {
1143
+ convert: ['solar2lunar', 'lunar2solar'],
1144
+ meihua: ['time', 'number'],
1145
+ xiaoliuren: ['time', 'number'],
1146
+ };
1147
+ const allowed = MODE_BY_SYSTEM[system];
1148
+ if (!allowed) {
1149
+ throw fail('E_INPUT', `mode 只用于 ${Object.keys(MODE_BY_SYSTEM).join(' / ')};${system || '当前体系'} 不收 mode`);
1150
+ }
1151
+ if (!allowed.includes(args.mode)) {
1152
+ throw fail('E_INPUT', `${system} 的 mode 只能是 ${allowed.join(' / ')},收到「${String(args.mode).slice(0, 24)}」`);
1153
+ }
1154
+ }
1155
+ // 风水 fortuneYear 以前只认整数:2026.5 被静默当成「没给」,整块年飞星消失。
1156
+ if (args.fortuneYear !== undefined && args.fortuneYear !== null && args.fortuneYear !== '') {
1157
+ const n = Number(args.fortuneYear);
1158
+ if (!Number.isInteger(n)) {
1159
+ throw fail('E_INPUT', `fortuneYear 要是整数年份,收到「${args.fortuneYear}」`);
1160
+ }
1161
+ args = { ...args, fortuneYear: n };
1162
+ }
1115
1163
  if (system === 'bazi') {
1116
1164
  const dt = parseDateTime(args.datetime);
1117
1165
  const asOf = checkAsOf(args.asOf);
@@ -1136,6 +1184,11 @@ function computeRaw(args = {}) {
1136
1184
  if (args.hourUnknown === true) {
1137
1185
  throw fail('HOUR_UNKNOWN', '紫微必须有出生时刻:命宫由生月与生时支定,时支未知则整盘不成立。请补上出生时刻,或改用八字三柱模式只看年/月/日。');
1138
1186
  }
1187
+ // 与奇门同一个别名口径:lateZi 是八字/六壬的参数名,紫微的历史参数名是 lateZishi。
1188
+ // 不归一的话 revise「晚子时算次日」会照常回执已应用,盘却按默认 current-day 排。
1189
+ if (args.lateZi !== undefined && args.lateZishi === undefined) {
1190
+ args = { ...args, lateZishi: args.lateZi === false ? 'current-day' : 'next-day' };
1191
+ }
1139
1192
  const dt = parseDateTime(args.datetime);
1140
1193
  const asOf = checkAsOf(args.asOf);
1141
1194
  const result = buildZiwei({
@@ -1243,6 +1296,9 @@ function computeRaw(args = {}) {
1243
1296
  }
1244
1297
  if (system === 'meihua') {
1245
1298
  let result;
1299
+ // 时间起卦必须与其它体系过同一把日期尺:以前把原串直接交给梅花私有解析器,
1300
+ // 1990-02-30 / 13 月 / 25:00 都会被接受,再被正规化成另一天起卦。
1301
+ if (args.mode !== 'number') parseDateTime(args.datetime);
1246
1302
  try {
1247
1303
  result = buildMeihua({
1248
1304
  mode: args.mode === 'number' ? 'number' : 'time',
@@ -1250,9 +1306,13 @@ function computeRaw(args = {}) {
1250
1306
  upper: args.upper,
1251
1307
  lower: args.lower,
1252
1308
  changing: args.changing,
1309
+ lateZi: args.lateZi,
1253
1310
  });
1254
1311
  } catch (e) {
1255
- throw fail('E_INPUT', `梅花起卦参数不完整:${e && e.message ? e.message : e}`);
1312
+ // 引擎自己定过码的(E_RANGE 越界 / E_INPUT 缺参)原样透传:一律压成 E_INPUT,
1313
+ // 调用方就分不清「换个时间重试」和「补参数」,「参数不完整」也是句假话——参数齐,只是年号出了表。
1314
+ const message = (e && e.message) || String(e);
1315
+ throw fail((e && e.code) || 'E_INPUT', (e && e.code) ? message : `梅花起卦参数不完整:${message}`);
1256
1316
  }
1257
1317
  return { ...result, summary: meihuaText(result) };
1258
1318
  }
@@ -1285,7 +1345,8 @@ function computeRaw(args = {}) {
1285
1345
  try {
1286
1346
  result = buildLiuyao({ lines, question: args.question, dayGanZhi });
1287
1347
  } catch (e) {
1288
- throw fail('E_LINES', e && e.message ? e.message : e);
1348
+ // 带码的原样透传:E_LINES 之外还有 E_INPUT(参数形状不对),压成 E_LINES 会把「没给爻值」说成「爻值不对」
1349
+ throw fail((e && e.code) || 'E_LINES', e && e.message ? e.message : e);
1289
1350
  }
1290
1351
  // 用神挂在结果上:摘要、面板、解析三层读同一份,避免各自取一次走散
1291
1352
  const ug = yongshenOf(result, { direction: args.direction, gender: args.gender });
@@ -1340,7 +1401,8 @@ function computeRaw(args = {}) {
1340
1401
  candidates: args.candidates,
1341
1402
  });
1342
1403
  } catch (e) {
1343
- throw fail('E_INPUT', (e && e.message) || String(e));
1404
+ // 同梅花:带码的(E_RANGE 越界)原样透传,别让择日的年份问题看起来像参数格式问题
1405
+ throw fail((e && e.code) || 'E_INPUT', (e && e.message) || String(e));
1344
1406
  }
1345
1407
  return { ...result, summary: zeriText(result) };
1346
1408
  }
@@ -1351,7 +1413,7 @@ function computeRaw(args = {}) {
1351
1413
  if (hour === null || hour < 0 || hour > 23) {
1352
1414
  throw fail('E_INPUT', '称骨必须给时辰:datetime 带时分(如 1990-08-15T10:00),或额外给 hour(0–23)——缺时辰四柱不齐');
1353
1415
  }
1354
- const result = buildChenggu({ year: dt.year, month: dt.month, day: dt.day, hour, minute: hasTime ? dt.minute : 0 });
1416
+ const result = buildChenggu({ year: dt.year, month: dt.month, day: dt.day, hour, minute: hasTime ? dt.minute : 0, lateZi: args.lateZi });
1355
1417
  return { ...result, summary: chengguText(result) };
1356
1418
  }
1357
1419
  if (system === 'convert') {
@@ -1384,6 +1446,7 @@ function computeRaw(args = {}) {
1384
1446
  num2: args.num2,
1385
1447
  num3: args.num3,
1386
1448
  question: args.question,
1449
+ lateZi: args.lateZi,
1387
1450
  });
1388
1451
  return { ...result, summary: xiaoliurenText(result) };
1389
1452
  }
@@ -1433,6 +1496,9 @@ function computeRaw(args = {}) {
1433
1496
  };
1434
1497
  }
1435
1498
  if (system === 'fengshui') {
1499
+ if (args.action != null && !['natal', 'advise', 'xuankong'].includes(args.action)) {
1500
+ throw fail('E_INPUT', `风水 action 只能是 natal / advise / xuankong,收到「${String(args.action).slice(0, 24)}」`);
1501
+ }
1436
1502
  const action = args.action === 'advise' ? 'advise' : args.action === 'xuankong' ? 'xuankong' : 'natal';
1437
1503
  if (action === 'xuankong') {
1438
1504
  const result = buildFengshui({
@@ -1440,7 +1506,7 @@ function computeRaw(args = {}) {
1440
1506
  sitting: args.sitting,
1441
1507
  facing: args.facing,
1442
1508
  ...(args.period !== undefined ? { period: args.period } : {}),
1443
- ...(Number.isInteger(args.fortuneYear) ? { fortuneYear: args.fortuneYear } : {}),
1509
+ ...(args.fortuneYear !== undefined ? { fortuneYear: args.fortuneYear } : {}),
1444
1510
  ...(typeof args.sittingOffset === 'number' ? { sittingOffset: args.sittingOffset } : {}),
1445
1511
  ...(typeof args.sittingDegree === 'number' ? { sittingDegree: args.sittingDegree } : {}),
1446
1512
  ...(typeof args.facingOffset === 'number' ? { facingOffset: args.facingOffset } : {}),
@@ -1467,12 +1533,15 @@ function computeRaw(args = {}) {
1467
1533
  action: 'natal',
1468
1534
  year: dt.year, month: dt.month, day: dt.day,
1469
1535
  gender: requireGender(args.gender),
1470
- fortuneYear: Number.isInteger(args.fortuneYear) ? args.fortuneYear : undefined,
1536
+ fortuneYear: args.fortuneYear,
1471
1537
  dst: args.dst !== false,
1472
1538
  });
1473
1539
  return { ...result, summary: fengshuiText(result) };
1474
1540
  }
1475
1541
  if (system === 'name') {
1542
+ if (args.action != null && !['assess', 'generate'].includes(args.action)) {
1543
+ throw fail('E_INPUT', `起名 action 只能是 assess / generate,收到「${String(args.action).slice(0, 24)}」`);
1544
+ }
1476
1545
  const action = args.action === 'generate' ? 'generate' : 'assess';
1477
1546
  const birth = args.datetime
1478
1547
  ? parseDateTime(args.datetime)
@@ -1865,7 +1934,7 @@ export { crossCheck, crossText, crossSection };
1865
1934
  export { buildAlmanac, buildZeri, buildChenggu, buildXiaoliuren, buildStar, buildNumerology, buildConvert };
1866
1935
  export { buildTrend, buildFengshui, buildName };
1867
1936
  export { buildTrace, traceMarkdown };
1868
- export { encodeHandle, decodeHandle, resolveHandle };
1937
+ export { encodeHandle, decodeHandle, resolveHandle, pruneForeignKeys, ownedKeys };
1869
1938
  // C3 对话桥替代:深链编解码、表单还原、三档复制包(面板与独立页共用一份)
1870
1939
  export { askLink, parseAskLink, formOf, restNote, askPackages };
1871
1940
  // 问事解读提示:把一张盘打包成「先答他所问、不堆术语、回到现实核对」的提示,供面板一键复制
@@ -427,10 +427,10 @@ export const KNOWLEDGE = [
427
427
  id: 'qimen-rumu',
428
428
  system: 'qimen', dims: ['overall'],
429
429
  gist: '天盘干落墓宫叫入墓:三奇与六仪各按五行墓库判。',
430
- detail: '按五行墓库推:乙木墓未(坤二)、丙丁火墓戌(乾六);六仪中戊墓戌(乾六)、己庚墓丑(艮八)、辛壬墓辰(巽四)、癸墓未(坤二)。天盘干落到自己的墓宫即入墓。取象是收束、藏起来、事情压住不动;好坏要看所问之事——问收藏、问了结,入墓未必是坏事。',
431
- source: '《奇门遁甲统宗》三奇入墓条;六仪入墓见《奇门遁甲秘笈大全》卷十五;五行墓库见《五行大义》',
430
+ detail: '按五行墓库推,乙、丁两个阴干从《玄机赋》逆行取墓说:乙墓戌(乾六)、丙墓戌(乾六)、丁墓丑(艮八);六仪中戊墓戌(乾六)、己庚墓丑(艮八)、辛壬墓辰(巽四)、癸墓未(坤二)。天盘干落到自己的墓宫即入墓。取象是收束、藏起来、事情压住不动;好坏要看所问之事——问收藏、问了结,入墓未必是坏事。',
431
+ source: '《奇门遁甲统宗》三奇入墓条;阴干逆行取墓见其中「玄机赋」;六仪入墓见《奇门遁甲秘笈大全》卷十五;五行墓库见《五行大义》',
432
432
  status: 'variant',
433
- note: '各家对入墓范围说法不一:有只论三奇,也有把六仪、八门、九星一并论墓的;本插件判天盘三奇与六仪,八门、九星暂不入墓。',
433
+ note: '各家对入墓范围说法不一:有只论三奇,也有把六仪、八门、九星一并论墓的;本插件判天盘三奇与六仪,八门、九星暂不入墓。墓宫取法同样有异说——通行另有一说按五行墓库直取(乙 木墓未、丁 火墓戌),本盘依《玄机赋》阴干逆行说(乙墓戌、丁墓丑),与引擎 qimen.js 的 RU_MU 表同源。',
434
434
  },
435
435
  {
436
436
  id: 'qimen-ganying',