dsh-metaphysics 0.57.0 → 0.59.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.
@@ -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/engine/zeri.js CHANGED
@@ -81,23 +81,68 @@ const VERDICT = (score, ctx) => {
81
81
  * @param {{task:string, startDate:string, days?:number, shengXiao?:string}} input
82
82
  * startDate 必填(公历 YYYY-MM-DD),避免引擎在无时钟环境下静默取「今天」。
83
83
  */
84
+ /** 严格解析公历日期:2 月 30 日这类不存在的日子直接判非法,不滚到下个月 */
85
+ function parseYmd(raw) {
86
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(raw || '').trim());
87
+ if (!m) return null;
88
+ const year = Number(m[1]);
89
+ const month = Number(m[2]);
90
+ const day = Number(m[3]);
91
+ if (month < 1 || month > 12 || day < 1) return null;
92
+ const back = fromJDN(toJDN(year, month, day));
93
+ return back.year === year && back.month === month && back.day === day ? { year, month, day } : null;
94
+ }
95
+
96
+ const ymd = (p) => `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day).padStart(2, '0')}`;
97
+
98
+ /**
99
+ * 择日主函数。
100
+ * @param {{task:string, startDate:string, days?:number, shengXiao?:string,
101
+ * candidates?:string[]}} input
102
+ * startDate 必填(公历 YYYY-MM-DD),避免引擎在无时钟环境下静默取「今天」。
103
+ * candidates 给了就只比这几个候选日(2–12 个,去重、保持输入顺序),startDate 与 days 不参与。
104
+ */
84
105
  export function buildZeri(input) {
85
106
  const task = input.task;
86
107
  if (!TASKS[task]) throw new Error(`不支持的事项「${task}」。支持:${Object.keys(TASKS).join('、')}`);
87
- const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(input.startDate || '').trim());
88
- if (!m) throw new Error('择日需要 startDate(公历 YYYY-MM-DD)——请向用户询问起算日,不要代填');
89
- const start = { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) };
90
- const days = Math.max(1, Math.min(60, Number.isInteger(input.days) ? input.days : 30));
91
108
  const shengXiaoRaw = typeof input.shengXiao === 'string' ? input.shengXiao.trim() : '';
92
109
  const shengXiao = shengXiaoRaw || null;
93
110
  if (shengXiao && !SHENGXIAO.includes(shengXiao)) throw new Error(`shengXiao 须为生肖名:${SHENGXIAO.join('、')}`);
94
111
 
112
+ // 候选日模式:只比用户给的这几天,不再按区间扫
113
+ let candidates = null;
114
+ if (input.candidates !== undefined && input.candidates !== null) {
115
+ if (!Array.isArray(input.candidates)) {
116
+ throw new Error('candidates 须为日期数组,例如 ["2026-12-01","2026-12-08","2026-12-15"]');
117
+ }
118
+ if (input.candidates.length < 2) throw new Error('candidates 至少给 2 个候选日;只想看一天请用 startDate + days: 1');
119
+ if (input.candidates.length > 12) throw new Error('candidates 最多 12 个候选日;更多请用 startDate + days 扫区间');
120
+ const seen = new Set();
121
+ candidates = [];
122
+ for (const raw of input.candidates) {
123
+ const p = parseYmd(raw);
124
+ if (!p) throw new Error(`候选日不是真实存在的公历日期:「${raw}」,请用 2026-12-01 这种格式`);
125
+ const key = ymd(p);
126
+ if (seen.has(key)) continue;
127
+ seen.add(key);
128
+ candidates.push(p);
129
+ }
130
+ if (candidates.length < 2) throw new Error('候选日去重后不足 2 天;至少要两个不同的日子才谈得上比较');
131
+ }
132
+
133
+ let start = null;
134
+ let days = 0;
135
+ if (!candidates) {
136
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(input.startDate || '').trim());
137
+ if (!m) throw new Error('择日需要 startDate(公历 YYYY-MM-DD)——请向用户询问起算日,不要代填');
138
+ start = { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) };
139
+ days = Math.max(1, Math.min(60, Number.isInteger(input.days) ? input.days : 30));
140
+ }
141
+
95
142
  const spec = TASKS[task];
96
143
  const zhixingSet = ZHIXING_FAVOR[spec.category];
97
- const startJdn = toJDN(start.year, start.month, start.day);
98
- const list = [];
99
- for (let i = 0; i < days; i++) {
100
- const p = fromJDN(startJdn + i);
144
+
145
+ const makeRow = (p) => {
101
146
  const a = buildAlmanac(p);
102
147
  let score = 0;
103
148
  const reasons = [];
@@ -114,7 +159,7 @@ export function buildZeri(input) {
114
159
  if (a.zhiXing === '危' && spec.category !== 'travel' && spec.category !== 'worship') { score -= 1; reasons.push('危日(-1)'); }
115
160
 
116
161
  const v = VERDICT(score, { chongSelf, jiHit });
117
- list.push({
162
+ return {
118
163
  date: a.meta.date,
119
164
  weekday: a.meta.weekday,
120
165
  lunar: a.lunar.text,
@@ -128,17 +173,47 @@ export function buildZeri(input) {
128
173
  verdict: v.verdict,
129
174
  verdictText: v.text,
130
175
  ...(v.excluded ? { excluded: true } : {}),
131
- });
132
- }
176
+ };
177
+ };
178
+
179
+ const list = candidates
180
+ ? candidates.map(makeRow)
181
+ : Array.from({ length: days }, (_, i) => makeRow(fromJDN(toJDN(start.year, start.month, start.day) + i)));
133
182
 
134
183
  const ranked = list.slice().sort((x, y) => y.score - x.score || (x.date < y.date ? -1 : 1));
184
+ const startDate = candidates
185
+ ? candidates.slice().sort((x, y) => (ymd(x) < ymd(y) ? -1 : 1)).map(ymd)[0]
186
+ : ymd(start);
187
+
188
+ // 候选日比较:只在这几天里比,给出最高分和同分者;不换算成「好多少」
189
+ let compare = null;
190
+ if (candidates) {
191
+ const pool = ranked.filter((d) => !d.excluded);
192
+ const top = pool[0] || null;
193
+ const scores = list.map((d) => d.score);
194
+ compare = {
195
+ mode: 'candidates',
196
+ count: list.length,
197
+ best: top ? {
198
+ date: top.date,
199
+ score: top.score,
200
+ verdict: top.verdict,
201
+ ties: pool.filter((d) => d.score === top.score).map((d) => d.date),
202
+ } : null,
203
+ spread: Math.max(...scores) - Math.min(...scores),
204
+ note: '候选日只在这几天里比:分数来自建除宜忌与冲煞,胜出只表示更合所问事项,不代表「好多少」。',
205
+ };
206
+ }
207
+
135
208
  return {
136
209
  system: 'zeri',
137
210
  meta: {
138
211
  task,
139
- startDate: `${start.year}-${String(start.month).padStart(2, '0')}-${String(start.day).padStart(2, '0')}`,
140
- days,
212
+ mode: candidates ? 'candidates' : 'range',
213
+ startDate,
214
+ days: list.length,
141
215
  shengXiao,
216
+ ...(candidates ? { candidates: candidates.map(ymd) } : {}),
142
217
  scope: shengXiao
143
218
  ? `宜忌按建除简表;已给当事人属相(${shengXiao}),凡当日冲该属相径直排除。评分仅用于排序引导。`
144
219
  : '宜忌按建除简表;未给当事人属相,未做冲属相排除。评分仅用于排序引导。',
@@ -147,6 +222,7 @@ export function buildZeri(input) {
147
222
  usable: ranked.filter((d) => d.verdict === '可用' && !d.excluded).slice(0, 5).map((d) => d.date),
148
223
  avoid: list.filter((d) => d.verdict === '回避').map((d) => `${d.date}(${d.reasons.join(';') || '综合减分'})`),
149
224
  days: list,
225
+ ...(compare ? { compare } : {}),
150
226
  excludedCount: list.filter((d) => d.excluded).length,
151
227
  sources: ['宜忌:建除十二神通行简表', '建除适配与冲煞:传统通行口径', '排序:宜忌加减分 + 属相排除(确定性)'],
152
228
  disclaimer: '择日为传统黄历口径的本地计算,属民俗参考;评分只用于排序,不构成择日结论。',
package/index.js CHANGED
@@ -230,11 +230,16 @@ export function apply(ctx) {
230
230
  },
231
231
  startDate: {
232
232
  type: 'string',
233
- description: '仅 zeri 使用:起算日(公历 YYYY-MM-DD)。省略则由宿主补今天。',
233
+ description: '仅 zeri 使用:起算日(公历 YYYY-MM-DD)。省略则由宿主补今天;给了 candidates 时不需要。',
234
234
  },
235
235
  days: {
236
236
  type: 'number',
237
- description: '仅 zeri 使用:往后考察天数,1–60,默认 30。',
237
+ description: '仅 zeri 使用:往后考察天数,1–60,默认 30;给了 candidates 时忽略。',
238
+ },
239
+ candidates: {
240
+ type: 'array',
241
+ items: { type: 'string' },
242
+ description: '仅 zeri 使用:候选日比较(2–12 个真实公历日期,如 ["2026-12-01","2026-12-08"])。给了就只比这几天,结果多一个 compare(这一组最优、同分者、分差)与逐日并排;不替代 startDate 扫区间。',
238
243
  },
239
244
  shengXiao: {
240
245
  type: 'string',
@@ -330,6 +335,17 @@ export function apply(ctx) {
330
335
  },
331
336
  required: ['datetime', 'gender'],
332
337
  },
338
+ partner2: {
339
+ type: 'object',
340
+ description: '三方合盘(可选):第三个人的出生信息,字段与 partner 相同。给了就多回 triple——你×对方、你×第三方、对方×第三方三对分数并列,不合成「三人总分」。仅 hepan 使用;null 与省略等价,不要传 null。',
341
+ properties: {
342
+ datetime: { type: 'string', description: '第三方公历本地时间,如 1988-09-09T08:00' },
343
+ gender: { type: 'string', enum: ['male', 'female'], description: '第三方性别' },
344
+ address: { type: 'string', description: '第三方出生地(省/市即可)' },
345
+ trueSolar: { type: 'boolean', description: '第三方是否启用真太阳时' },
346
+ },
347
+ required: ['datetime', 'gender'],
348
+ },
333
349
  mode: {
334
350
  type: 'string',
335
351
  enum: ['time', 'number'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-metaphysics",
3
- "version": "0.57.0",
3
+ "version": "0.59.0",
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, 391-char name library with per-character pinyin, Kangxi stroke counts, five-grid (五格/三才) analysis and mechanical phonetics checks, 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 (闰月按日切分/整月归次月/整月归本月) nine school switches (亮度/庚干四化/天马/天空/截空旬空/魁钺/天伤天使/长生/晚子时) for ziwei, 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": [
@@ -74,7 +74,12 @@
74
74
  "check:bundle": "node tools/check-bundle.mjs",
75
75
  "check:patterns": "node tools/check-patterns.mjs",
76
76
  "check:perf": "node tools/check-perf.mjs",
77
- "release": "node tools/release.mjs"
77
+ "gen:zengshan": "node tools/gen-zengshan-cases.mjs",
78
+ "backtest:yingqi": "node tools/backtest-yingqi.mjs",
79
+ "release": "node tools/release.mjs",
80
+ "push:tag": "node tools/push-tag.mjs",
81
+ "rollback:plan": "node tools/release-rollback.mjs",
82
+ "prompt:event": "node tools/event-prompt.mjs"
78
83
  },
79
84
  "exports": {
80
85
  ".": "./index.js",