dsh-metaphysics 0.15.0 → 0.17.0
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/CHANGELOG.md +40 -0
- package/README.md +11 -0
- package/client.js +1028 -25
- package/engine/crosscheck.js +133 -0
- package/engine/followup.js +173 -0
- package/engine/geo.js +1 -1
- package/engine/index.js +99 -4
- package/engine/interpret.js +126 -10
- package/engine/rectify.js +130 -0
- package/engine/verify.js +64 -0
- package/index.js +28 -1
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// 生时校正(仅八字):给一组「过去发生过的事」,把候选时辰逐一排盘、按同一套信号规则打分,
|
|
2
|
+
// 输出候选排序,帮助缩小出生时辰的范围。
|
|
3
|
+
//
|
|
4
|
+
// 必须说清的三件事:
|
|
5
|
+
// 1. 这是**缩小范围**的参考,不替代出生记录;命中数相同或接近时,不要硬选一个;
|
|
6
|
+
// 2. 只有时柱(以及跨 23:00 时的日柱)会变,年/月柱通常不变,所以候选之间的差异集中在晚运、子女、事业后半段;
|
|
7
|
+
// 3. 打分规则是显式、固定的(见 matchEvent),不是拟合;换一套规则排序可能变。
|
|
8
|
+
import { buildBazi } from './bazi.js';
|
|
9
|
+
|
|
10
|
+
const KINDS = ['career', 'wealth', 'love', 'move', 'health', 'other'];
|
|
11
|
+
const HOURS = [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23];
|
|
12
|
+
|
|
13
|
+
const TEN_GOD = (l) => l.stemTenGod || '';
|
|
14
|
+
const has = (l, re) => re.test((l.relations || []).join(''));
|
|
15
|
+
const tier = (l) => (l.luck && l.luck.tier) || '平';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 单条「过去发生的事」在某个候选盘里的信号是否吻合(规则固定、可复核)。
|
|
19
|
+
* @param {object} l 某年的流年条目
|
|
20
|
+
* @param {string} kind career | wealth | love | move | health | other
|
|
21
|
+
*/
|
|
22
|
+
export function matchEvent(l, kind) {
|
|
23
|
+
if (!l) return false;
|
|
24
|
+
const tg = TEN_GOD(l);
|
|
25
|
+
const t = tier(l);
|
|
26
|
+
switch (kind) {
|
|
27
|
+
case 'career': return /官|杀|食|伤/.test(tg) || has(l, /冲|合/) || t === '大吉' || t === '大忌';
|
|
28
|
+
case 'wealth': return /财|食|伤/.test(tg) || t === '大吉' || t === '吉';
|
|
29
|
+
case 'love': return /财|官|杀/.test(tg) || has(l, /合|冲/);
|
|
30
|
+
case 'move': return has(l, /冲|合/);
|
|
31
|
+
case 'health': return has(l, /刑|害|冲/) || t === '大忌' || t === '忌';
|
|
32
|
+
default: return t === '大吉' || t === '大忌' || has(l, /冲|刑|合/);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} opts
|
|
38
|
+
* @param {{year:number,month:number,day:number,minute?:number}} opts.base 出生日期(时辰待定)
|
|
39
|
+
* @param {'male'|'female'} opts.gender
|
|
40
|
+
* @param {Array<{year:number, kind:string}>} opts.events 过去发生的事(年份 + 类别)
|
|
41
|
+
* @param {number[]} [opts.hours] 候选小时(默认 0/1/2/4/…/22/23)
|
|
42
|
+
* @param {boolean} [opts.lateZi] 晚子时是否换日
|
|
43
|
+
* @param {boolean} [opts.trueSolar]
|
|
44
|
+
* @param {string} [opts.address]
|
|
45
|
+
* @param {number} [opts.longitude]
|
|
46
|
+
* @param {object} [opts.geo]
|
|
47
|
+
*/
|
|
48
|
+
export function rectifyBirth(opts) {
|
|
49
|
+
const base = opts && opts.base;
|
|
50
|
+
const events = (opts && opts.events) || [];
|
|
51
|
+
const gender = opts && opts.gender;
|
|
52
|
+
if (!base || !Number.isInteger(base.year) || !Number.isInteger(base.month) || !Number.isInteger(base.day)) {
|
|
53
|
+
const e = new Error('生时校正需要出生日期 { year, month, day }');
|
|
54
|
+
e.code = 'E_INPUT';
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
if (!gender) {
|
|
58
|
+
const e = new Error('生时校正需要性别');
|
|
59
|
+
e.code = 'E_INPUT';
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
const clean = events
|
|
63
|
+
.filter((x) => x && Number.isInteger(Number(x.year)))
|
|
64
|
+
.map((x) => ({ year: Number(x.year), kind: KINDS.includes(x.kind) ? x.kind : 'other' }));
|
|
65
|
+
if (!clean.length) {
|
|
66
|
+
const e = new Error('生时校正需要至少一条「过去发生的事」(年份 + 类别)');
|
|
67
|
+
e.code = 'E_INPUT';
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
const nowYear = Math.min(...clean.map((x) => x.year));
|
|
71
|
+
const hours = Array.isArray(opts.hours) && opts.hours.length
|
|
72
|
+
? opts.hours.filter((h) => Number.isInteger(h) && h >= 0 && h <= 23)
|
|
73
|
+
: HOURS;
|
|
74
|
+
|
|
75
|
+
const candidates = hours.map((hour) => {
|
|
76
|
+
const chart = buildBazi({
|
|
77
|
+
...base,
|
|
78
|
+
hour,
|
|
79
|
+
minute: 0,
|
|
80
|
+
gender,
|
|
81
|
+
lateZi: opts.lateZi !== false,
|
|
82
|
+
trueSolar: opts.trueSolar !== false,
|
|
83
|
+
address: typeof opts.address === 'string' ? opts.address : undefined,
|
|
84
|
+
longitude: typeof opts.longitude === 'number' ? opts.longitude : undefined,
|
|
85
|
+
geo: opts.geo && typeof opts.geo === 'object' ? opts.geo : undefined,
|
|
86
|
+
nowYear,
|
|
87
|
+
});
|
|
88
|
+
const matched = [];
|
|
89
|
+
const missed = [];
|
|
90
|
+
for (const ev of clean) {
|
|
91
|
+
const l = (chart.liunian || []).find((x) => Number(x.year) === ev.year);
|
|
92
|
+
(matchEvent(l, ev.kind) ? matched : missed).push(ev);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
hour,
|
|
96
|
+
label: `${String(hour).padStart(2, '0')}:00 起`,
|
|
97
|
+
dayGanzhi: chart.pillars.day.ganzhi,
|
|
98
|
+
hourGanzhi: chart.pillars.hour.ganzhi,
|
|
99
|
+
score: clean.length ? matched.length / clean.length : 0,
|
|
100
|
+
hits: matched.length,
|
|
101
|
+
total: clean.length,
|
|
102
|
+
matchedYears: matched.map((x) => x.year),
|
|
103
|
+
missedYears: missed.map((x) => x.year),
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
candidates.sort((a, b) => b.score - a.score || a.hour - b.hour);
|
|
107
|
+
|
|
108
|
+
const days = [...new Set(candidates.map((c) => c.dayGanzhi))];
|
|
109
|
+
const best = candidates[0];
|
|
110
|
+
const second = candidates[1];
|
|
111
|
+
const tie = second && second.score === best.score;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
system: 'bazi',
|
|
115
|
+
base,
|
|
116
|
+
events: clean,
|
|
117
|
+
hours,
|
|
118
|
+
candidates,
|
|
119
|
+
best: best || null,
|
|
120
|
+
tie,
|
|
121
|
+
dayChanged: days.length > 1,
|
|
122
|
+
note: `候选 ${candidates.length} 个:命中数最高 ${best ? best.hits : 0}/${clean.length}${tie ? `(有并列:${candidates.filter((c) => c.score === best.score).map((c) => c.hour).join('、')} 点)` : ''}。` +
|
|
123
|
+
(days.length > 1 ? `注意:跨 23:00 的候选会改变日柱(${days.join(' / ')}),这类差异比时柱更关键。` : '候选之间只差时柱(日柱未变)。') +
|
|
124
|
+
' 这是缩小范围的参考,不替代出生记录;命中数接近时不要硬选。',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 事件类别(与工具 schema 的 kind 枚举一致);导出时不起别名,打包器只支持直接导出 */
|
|
129
|
+
export const RECTIFY_KINDS = KINDS;
|
|
130
|
+
export default rectifyBirth;
|
package/engine/verify.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// 验前事:把过去几年的流年信号摊开,让用户自己标「准 / 不准 / 不确定」,
|
|
2
|
+
// 只算一个贴合度提示,**不改盘、不重排、不自动调整结论**。
|
|
3
|
+
//
|
|
4
|
+
// 口径说明:这里说的「准」是用户对自己经历的主观确认,不是事实核验;
|
|
5
|
+
// 贴合度低时优先怀疑输入(出生时间、时区、晚子时口径),而不是推翻整张盘。
|
|
6
|
+
const KIND_LABEL = {
|
|
7
|
+
career: '事业变动',
|
|
8
|
+
wealth: '财运起伏',
|
|
9
|
+
love: '感情变化',
|
|
10
|
+
move: '搬迁 / 出行',
|
|
11
|
+
health: '健康问题',
|
|
12
|
+
other: '其他大事',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const THEME = (l) => {
|
|
16
|
+
const rel = (l.relations || []).join('');
|
|
17
|
+
if (/冲/.test(rel)) return '有冲:变动 / 摩擦';
|
|
18
|
+
if (/刑/.test(rel)) return '有刑:内耗 / 反复';
|
|
19
|
+
if (/合/.test(rel)) return '有合:合作 / 牵引';
|
|
20
|
+
const tier = l.luck && l.luck.tier;
|
|
21
|
+
if (tier === '大吉' || tier === '吉') return '流年偏顺';
|
|
22
|
+
if (tier === '大忌' || tier === '忌') return '流年偏压';
|
|
23
|
+
return '信号平缓';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** 把某张盘(需已含过去年份的 liunian)摊成可勾选的信号行 */
|
|
27
|
+
export function pastSignals(chart, untilYear) {
|
|
28
|
+
const until = Number(untilYear) || Infinity;
|
|
29
|
+
return (chart.liunian || [])
|
|
30
|
+
.filter((l) => Number(l.year) <= until)
|
|
31
|
+
.map((l) => ({
|
|
32
|
+
year: Number(l.year),
|
|
33
|
+
ganzhi: l.ganzhi,
|
|
34
|
+
tier: (l.luck && l.luck.tier) || '平',
|
|
35
|
+
tenGod: l.stemTenGod || '',
|
|
36
|
+
relations: l.relations || [],
|
|
37
|
+
theme: THEME(l),
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 按用户的勾选算贴合度。
|
|
43
|
+
* @param {Array<{year:number}>} rows pastSignals 的结果
|
|
44
|
+
* @param {Record<string,'yes'|'no'|'unsure'>} marks 年份 → 勾选
|
|
45
|
+
*/
|
|
46
|
+
export function verifyFit(rows, marks) {
|
|
47
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
48
|
+
const m = marks || {};
|
|
49
|
+
const picked = list.filter((r) => m[r.year] === 'yes' || m[r.year] === 'no');
|
|
50
|
+
const yes = picked.filter((r) => m[r.year] === 'yes').length;
|
|
51
|
+
const no = picked.length - yes;
|
|
52
|
+
const rate = picked.length ? yes / picked.length : null;
|
|
53
|
+
const level = rate == null ? '未评' : rate >= 0.7 ? '高' : rate >= 0.4 ? '中' : '低';
|
|
54
|
+
let advice = '';
|
|
55
|
+
if (rate == null) advice = '先勾几项「准 / 不准 / 不确定」,再看得出的贴合度。';
|
|
56
|
+
else if (level === '高') advice = '前事与流年信号对得较顺,可以按这张盘继续看;有分歧时以事实为准。';
|
|
57
|
+
else if (level === '中') advice = '有一部分对得上:先用「追问」核对具体年份,再决定信到什么程度。';
|
|
58
|
+
else advice = '对得不多:优先核对出生时间、出生地(时区/真太阳时)与晚子时口径,而不是改结论。';
|
|
59
|
+
return { answered: picked.length, yes, no, rate, level, advice, labels: { ...KIND_LABEL } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { KIND_LABEL };
|
|
63
|
+
|
|
64
|
+
export default pastSignals;
|
package/index.js
CHANGED
|
@@ -142,7 +142,34 @@ export function apply(ctx) {
|
|
|
142
142
|
enum: ['general', 'wealth', 'career', 'love', 'health', 'study', 'travel'],
|
|
143
143
|
description: '询问方向(仅配合 withInterpret 生效):general 综合、wealth 财运、career 事业、love 感情、health 健康、study 学业、travel 出行。默认 general。',
|
|
144
144
|
},
|
|
145
|
-
|
|
145
|
+
cross: {
|
|
146
|
+
type: 'boolean',
|
|
147
|
+
description: '八字 / 紫微专用:为同一份出生信息再排另一套盘,返回「八字 × 紫微 交叉验证」(同向 / 有张力 / 互补),不合并数值。',
|
|
148
|
+
},
|
|
149
|
+
verifyPast: {
|
|
150
|
+
type: 'number',
|
|
151
|
+
description: '验前事(仅八字):传年数(如 6),返回过去 N 年的流年信号行,供用户勾选「准/不准」并算贴合度;只读,不改盘。',
|
|
152
|
+
},
|
|
153
|
+
rectify: {
|
|
154
|
+
type: 'boolean',
|
|
155
|
+
description: '生时校正(仅八字):配合 events 使用,把候选时辰逐一排盘并按固定规则对过去事件打分,返回候选排序;用于缩小时辰范围,不替代出生记录。',
|
|
156
|
+
},
|
|
157
|
+
events: {
|
|
158
|
+
type: 'array',
|
|
159
|
+
description: '生时校正用的过去事件:年份 + 类别(career/wealth/love/move/health/other)。',
|
|
160
|
+
items: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
year: { type: 'integer', description: '事件发生的公历年份,如 2019' },
|
|
164
|
+
kind: { type: 'string', enum: ['career', 'wealth', 'love', 'move', 'health', 'other'], description: '事件类别' },
|
|
165
|
+
},
|
|
166
|
+
required: ['year'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
ask: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: '追问:围绕已算好的盘回答「某一年 / 某方面」的问题,如「2027 年事业怎么样」「感情如何」。只取盘面已有字段,不重排、不猜测。',
|
|
172
|
+
}, withInterpret: {
|
|
146
173
|
type: 'boolean',
|
|
147
174
|
description: '是否附带「解析与详解」(五个维度 + 用神/格局等分节 + 免责声明);默认 false,避免默认输出过长。',
|
|
148
175
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-metaphysics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "命理排盘 for DeepSeek Harness: eight systems — BaZi four pillars (42 shensha, pattern / useful-god / luck scoring), Zi Wei Dou Shu (34 patterns, 大限, 流年/流月/流日/流时四化), LiuYao with real line rendering, MeiHua YiShu, Da Liu Ren, Qi Men Dun Jia (chaibu / chaoshen / zhirun), compatibility charts (BaZi + Zi Wei) and Tarot; Ask(所问之事 + 询问方向)driven interpretation, plus a plain-language 解析与详解 module. Reproducible via seed/asOf. All computed locally, networking off by default — as an agent tool plus a Web panel.",
|
|
6
6
|
"keywords": [
|