dsh-metaphysics 0.59.0 → 0.60.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/engine/rectify.js CHANGED
@@ -9,6 +9,8 @@ import { buildBazi } from './bazi.js';
9
9
 
10
10
  const KINDS = ['career', 'wealth', 'love', 'move', 'health', 'other'];
11
11
  const HOURS = [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23];
12
+ /** 事件条数上限:对账用不到几百条,超限是输入异常而不是需求 */
13
+ const MAX_EVENTS = 200;
12
14
 
13
15
  const TEN_GOD = (l) => l.stemTenGod || '';
14
16
  const has = (l, re) => re.test((l.relations || []).join(''));
@@ -67,10 +69,29 @@ export function rectifyBirth(opts) {
67
69
  e.code = 'E_INPUT';
68
70
  throw e;
69
71
  }
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)
72
+ // 事件只用来对账,几十条已足够。不设上限的后果很具体:20 万条会原样回显成 2MB 结果,
73
+ // 并且下面的取最小值(Math.min(...arr) 逐元素入参)直接把栈打爆。
74
+ if (clean.length > MAX_EVENTS) {
75
+ const e = new Error(`生时校正的事件最多 ${MAX_EVENTS} 条,收到 ${clean.length} 条——只报最近几十年里确实发生过的事即可`);
76
+ e.code = 'E_INPUT';
77
+ throw e;
78
+ }
79
+ if (clean.some((x) => x.year < 1600 || x.year > 2400)) {
80
+ const e = new Error('事件年份要落在 1600–2400 之间(与排盘支持的年份一致)');
81
+ e.code = 'E_INPUT';
82
+ throw e;
83
+ }
84
+ const nowYear = clean.reduce((m, x) => (x.year < m ? x.year : m), clean[0].year);
85
+ // 候选时辰同样要限:一天只有 12 个时辰,给了 1000 个就是白排 1000 张盘
86
+ const wanted = Array.isArray(opts.hours) && opts.hours.length
87
+ ? [...new Set(opts.hours.filter((h) => Number.isInteger(h) && h >= 0 && h <= 23))]
73
88
  : HOURS;
89
+ if (!wanted.length) {
90
+ const e = new Error('候选时辰里没有有效的小时(0–23 的整数)');
91
+ e.code = 'E_INPUT';
92
+ throw e;
93
+ }
94
+ const hours = wanted.slice(0, 24);
74
95
 
75
96
  const candidates = hours.map((hour) => {
76
97
  const chart = buildBazi({
@@ -96,7 +117,8 @@ export function rectifyBirth(opts) {
96
117
  hour,
97
118
  label: `${String(hour).padStart(2, '0')}:00 起`,
98
119
  dayGanzhi: chart.pillars.day.ganzhi,
99
- hourGanzhi: chart.pillars.hour.ganzhi,
120
+ // 时辰未知时没有时柱可校正:给 null,让调用方自己判断要不要做这件事
121
+ hourGanzhi: chart.pillars.hour ? chart.pillars.hour.ganzhi : null,
100
122
  score: clean.length ? matched.length / clean.length : 0,
101
123
  hits: matched.length,
102
124
  total: clean.length,
package/engine/trend.js CHANGED
@@ -44,21 +44,37 @@ function tenGodOfZhi(dayStem, branch) {
44
44
 
45
45
  const yearGanzhi = (year) => ganzhiName(((year - 4) % 60 + 60) % 60);
46
46
 
47
- /** 把大运表补到覆盖 toAge(引擎默认只排 8 步 / 80 年)。 */
48
- function extendDayun(list, direction, toAge) {
49
- const out = list.slice();
47
+ /**
48
+ * 大运分段,按「交运那一年」切,年龄落在与 points[].age 同一条虚岁轴上。
49
+ *
50
+ * 不能直接用 dayun.startAge:那是周岁(三日用折一年从出生日算起),
51
+ * 而 K 线一年一格、`age` 就是虚岁(= 年份 − 出生年 + 1,与八字流年表同一口径)。
52
+ * 两套轴混在一起会让整条线提前一年:1990-05-12 男命 1998-08 交壬午,
53
+ * 旧写法把 1997(还没起运)划进壬午,2007(仍在壬午)却标成癸未。
54
+ * 交运日期本来就是同一份数据的精确表达,所以按它的年份起算;
55
+ * 引擎只排八步,八步之外按十年一格推年份,干支仍按六十甲子顺/逆推。
56
+ */
57
+ function buildSegments(list, direction, birthYear, toAge) {
58
+ const toYear = birthYear + toAge - 1;
50
59
  const step = direction === '逆行' ? -1 : 1;
60
+ const years = [];
61
+ for (const d of list) years.push({ ganZhi: d.ganzhi, startYear: Number(String(d.fromDate).slice(0, 4)) });
51
62
  let guard = 0;
52
- while (out.length && out[out.length - 1].startAge <= toAge && guard < 24) {
53
- const last = out[out.length - 1];
54
- // last.ganzhi 是「甲辰」这样的名字,不是六十甲子序号:先换回序号再顺/逆推一步
55
- const lastIdx = sexagenaryIndex(STEMS.indexOf(last.ganzhi[0]), BRANCHES.indexOf(last.ganzhi[1]));
56
- const nextIdx = (lastIdx + step + 60) % 60;
57
- const g = ganzhiName(nextIdx);
58
- out.push({ step: last.step + 1, startAge: last.startAge + 10, ganzhi: g.name, stem: g.stem, branch: g.branch, stemTenGod: last.stemTenGod });
63
+ while (years.length && years[years.length - 1].startYear <= toYear && guard < 24) {
64
+ const last = years[years.length - 1];
65
+ // ganZhi 是「甲辰」这样的名字,不是六十甲子序号:先换回序号再顺/逆推一步
66
+ const lastIdx = sexagenaryIndex(STEMS.indexOf(last.ganZhi[0]), BRANCHES.indexOf(last.ganZhi[1]));
67
+ const g = ganzhiName((lastIdx + step + 60) % 60);
68
+ years.push({ ganZhi: g.name, startYear: last.startYear + 10 });
59
69
  guard++;
60
70
  }
61
- return out;
71
+ return years
72
+ .map((s, i) => ({
73
+ ganZhi: s.ganZhi,
74
+ startAge: s.startYear - birthYear + 1,
75
+ endAge: (years[i + 1] ? years[i + 1].startYear : s.startYear + 10) - birthYear,
76
+ }))
77
+ .filter((s) => s.startAge <= toAge);
62
78
  }
63
79
 
64
80
  /**
@@ -94,16 +110,9 @@ export function buildTrend(input) {
94
110
  const scoreOf = (god) => table[god] ?? 0;
95
111
  const boundary = LEVEL_CUTS.some((c) => Math.abs(r.strength.score - c) <= BOUNDARY_EPS);
96
112
 
97
- const dayun = extendDayun(r.dayun.list, r.dayun.direction, toAge);
98
- const segments = dayun
99
- .map((d, i) => ({
100
- ganZhi: d.ganzhi,
101
- startAge: d.startAge,
102
- endAge: (dayun[i + 1] ? dayun[i + 1].startAge : d.startAge + 10) - 1,
103
- }))
104
- .filter((s) => s.startAge <= toAge);
105
-
106
113
  const birthYear = Number(String(r.solarTime.civilText).slice(0, 4));
114
+ const segments = buildSegments(r.dayun.list, r.dayun.direction, birthYear, toAge);
115
+
107
116
  const points = [];
108
117
  let prev = 50;
109
118
  for (let age = 1; age <= toAge; age++) {
@@ -145,6 +154,9 @@ export function buildTrend(input) {
145
154
  dayMaster: `${dayStem}(${r.dayMaster.yinYang}${r.dayMaster.element})`,
146
155
  strength: { level: r.strength.level, score: r.strength.score, boundary },
147
156
  toAge,
157
+ // 当前年份由调用方给(asOf):引擎不读挂钟,同一条线在任意时刻复算都一致。
158
+ // 没给就不写这个键,summary 那边据此省掉「你现在在第几步」。
159
+ ...(Number.isInteger(input.nowYear) ? { nowYear: input.nowYear } : {}),
148
160
  ...(boundary
149
161
  ? {
150
162
  boundaryNote:
package/engine/zeri.js CHANGED
@@ -2,6 +2,11 @@
2
2
  // 口径透明:这是排序引导,不是吉凶断语;未收录逐日神煞,冲煞按日支对生肖。
3
3
  import { fromJDN, toJDN } from './calendar.js';
4
4
  import { buildAlmanac, SHENGXIAO } from './almanac.js';
5
+ import { fail } from './fail.js';
6
+
7
+ // 择日的入参错误一律带 E_INPUT:裸 new Error 到工具层只剩 [E_INTERNAL],
8
+ // 模型拿到的是一句没有指向性的文案,改不了参数。
9
+ const bad = (message) => fail('E_INPUT', message);
5
10
 
6
11
  /** 支持的事项:直接宜词 + 关联宜词(黄历措辞)。 */
7
12
  export const TASKS = {
@@ -104,39 +109,50 @@ const ymd = (p) => `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day
104
109
  */
105
110
  export function buildZeri(input) {
106
111
  const task = input.task;
107
- if (!TASKS[task]) throw new Error(`不支持的事项「${task}」。支持:${Object.keys(TASKS).join('、')}`);
112
+ if (!TASKS[task]) throw bad(`不支持的事项「${task}」。支持:${Object.keys(TASKS).join('、')}`);
108
113
  const shengXiaoRaw = typeof input.shengXiao === 'string' ? input.shengXiao.trim() : '';
109
114
  const shengXiao = shengXiaoRaw || null;
110
- if (shengXiao && !SHENGXIAO.includes(shengXiao)) throw new Error(`shengXiao 须为生肖名:${SHENGXIAO.join('、')}`);
115
+ if (shengXiao && !SHENGXIAO.includes(shengXiao)) throw bad(`shengXiao 须为生肖名:${SHENGXIAO.join('、')}`);
111
116
 
112
117
  // 候选日模式:只比用户给的这几天,不再按区间扫
113
118
  let candidates = null;
114
119
  if (input.candidates !== undefined && input.candidates !== null) {
115
120
  if (!Array.isArray(input.candidates)) {
116
- throw new Error('candidates 须为日期数组,例如 ["2026-12-01","2026-12-08","2026-12-15"]');
121
+ throw bad('candidates 须为日期数组,例如 ["2026-12-01","2026-12-08","2026-12-15"]');
117
122
  }
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 扫区间');
123
+ if (input.candidates.length < 2) throw bad('candidates 至少给 2 个候选日;只想看一天请用 startDate + days: 1');
124
+ if (input.candidates.length > 12) throw bad('candidates 最多 12 个候选日;更多请用 startDate + days 扫区间');
120
125
  const seen = new Set();
121
126
  candidates = [];
122
127
  for (const raw of input.candidates) {
123
128
  const p = parseYmd(raw);
124
- if (!p) throw new Error(`候选日不是真实存在的公历日期:「${raw}」,请用 2026-12-01 这种格式`);
129
+ if (!p) throw bad(`候选日不是真实存在的公历日期:「${raw}」,请用 2026-12-01 这种格式`);
125
130
  const key = ymd(p);
126
131
  if (seen.has(key)) continue;
127
132
  seen.add(key);
128
133
  candidates.push(p);
129
134
  }
130
- if (candidates.length < 2) throw new Error('候选日去重后不足 2 天;至少要两个不同的日子才谈得上比较');
135
+ if (candidates.length < 2) throw bad('候选日去重后不足 2 天;至少要两个不同的日子才谈得上比较');
131
136
  }
132
137
 
133
138
  let start = null;
134
139
  let days = 0;
135
140
  if (!candidates) {
136
141
  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)——请向用户询问起算日,不要代填');
142
+ if (!m) throw bad('择日需要 startDate(公历 YYYY-MM-DD)——请向用户询问起算日,不要代填');
138
143
  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));
144
+ // days 以前只认 Number.isInteger,于是 "45"(模型很爱给字符串)与 45.5 都静默变成 30 天:
145
+ // 用户要的区间被悄悄改短,而结果里只看得到实际扫了几天,看不出被改过。
146
+ // 现在:能规范成整数的(含数字串)就规范,范围外或非数字一律报错,不猜。
147
+ const rawDays = input.days;
148
+ if (rawDays === undefined || rawDays === null || rawDays === '') {
149
+ days = 30;
150
+ } else {
151
+ const n = Number(rawDays);
152
+ if (!Number.isInteger(n)) throw bad(`择日的 days 要是 1–60 的整数,收到「${rawDays}」`);
153
+ if (n < 1 || n > 60) throw bad(`择日的 days 要在 1–60 之间(${n} 超出范围);更长请用分段多次查询`);
154
+ days = n;
155
+ }
140
156
  }
141
157
 
142
158
  const spec = TASKS[task];
package/engine/ziwei.js CHANGED
@@ -647,8 +647,11 @@ export function buildZiwei(input) {
647
647
  };
648
648
  })();
649
649
 
650
- // 流年(当年起 12 年)
651
- const thisYear = new Date().getFullYear();
650
+ // 流年(基准年起 12 年):基准年优先用调用方传来的 asOf,缺省才落本机当年。
651
+ // 直接读挂钟的话,同一组参数(同一个句柄)跨年会排出不同的流年表,
652
+ // 「同参数同盘」的承诺就只在当年成立。
653
+ const asOfForLiunian = asOfParts(input.asOf);
654
+ const thisYear = (asOfForLiunian && asOfForLiunian.year) || new Date().getFullYear();
652
655
  const liunian = [];
653
656
  for (let y = thisYear; y < thisYear + 12; y++) {
654
657
  const gz = ganzhiName((((y - 4) % 60) + 60) % 60);
package/index.js CHANGED
@@ -33,9 +33,10 @@ 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(询问方向)。',
36
+ '要给「结合所问之事的解读」时,把 withInterpret 设为 true,并尽量带上 question(所问之事)与 direction(询问方向);只要结论与时间表就再带 detail: "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
+ '出生时刻未知时不要编一个钟点:传 hourUnknown: true——八字只排年/月/日三柱(时柱暂缺,摘要首行提示),趋势按日降级,紫微与合盘会直接以 HOUR_UNKNOWN 阻断并给出补时刻的引导。',
39
40
  '结果可复现:tarot / liuyao 传 seed,紫微 / 八字 / 塔罗传 asOf;输出摘要会写明晚子时与真太阳时实际采用的口径。需要回放或对拍时传 shadow: true,结果带影子盘 ID(同参数同 ID;baseId 不含版本,可跨版本比对字段变化)。要看结论是怎么推出来的,传 trace: true:六爻给纳甲→六亲→世应→伏神→旺衰,八字给旺衰分值与格局取用,紫微给格局命中条件,奇门与大六壬给盘面层与取用层;同时回传 traceText(同一份层的 Markdown,可直接贴进对话或文档)。',
40
41
  '结果用于传统文化与娱乐参考,不得用于医疗、投资、法律、生死等确定性断言。',
41
42
  ].join('');
@@ -58,7 +59,8 @@ export function apply(ctx) {
58
59
  const hit = geoCache.get(key);
59
60
  if (hit !== undefined) return hit;
60
61
  const value = await geocodeAddress(address, opts);
61
- geoCache.set(key, value);
62
+ // 联网失败不写缓存:否则会连着 6 小时把「解析失败」当成答案,网络恢复也翻不了身
63
+ if (value && !value.onlineFailed) geoCache.set(key, value);
62
64
  return value;
63
65
  }
64
66
 
@@ -93,6 +95,18 @@ export function apply(ctx) {
93
95
  };
94
96
  }
95
97
  if (next.shadow === true && !next.shadowVersion) next.shadowVersion = PKG_VERSION;
98
+ // 自由文本长度上限:这些串会整段进句柄(handle 是参数的 base64url),
99
+ // 不设限就等于允许一次调用把结果撑到几百 KB——摘要、句柄、影子盘四处都要重抄一遍。
100
+ const MAX_LEN = { address: 80, question: 300, revise: 200, ask: 200, surname: 16, givenName: 16 };
101
+ for (const [k, lim] of Object.entries(MAX_LEN)) {
102
+ if (typeof next[k] === 'string' && next[k].length > lim) {
103
+ throw bad(`${k} 最长 ${lim} 个字符(收到 ${next[k].length} 个);写要点即可,例如「成都」「今年是否适合换工作」`);
104
+ }
105
+ }
106
+ if (next.partner && typeof next.partner === 'object' && typeof next.partner.address === 'string'
107
+ && next.partner.address.length > MAX_LEN.address) {
108
+ throw bad(`partner.address 最长 ${MAX_LEN.address} 个字符(收到 ${next.partner.address.length} 个)`);
109
+ }
96
110
  // 默认离线:内置表覆盖到地级市,足够真太阳时用;联网是把出生地原文发给第三方,必须显式开启
97
111
  const wantsOnline = next.geocode === 'online';
98
112
  const resolveGeo = async (person) => {
@@ -109,8 +123,10 @@ export function apply(ctx) {
109
123
  // 联网失败不影响排盘,退回内置表
110
124
  }
111
125
  };
126
+ // fengshui 不在名单里:玄空/八宅只看 year/month/day/gender 与坐向,
127
+ // 经度对盘面毫无影响,把出生地发给第三方就是纯外泄。
112
128
  if (next.system === 'bazi' || next.system === 'ziwei' || next.system === 'hepan'
113
- || next.system === 'trend' || next.system === 'fengshui' || next.system === 'name') {
129
+ || next.system === 'trend' || next.system === 'name') {
114
130
  await resolveGeo(next);
115
131
  }
116
132
  if (next.system === 'hepan' && next.partner && typeof next.partner === 'object') {
@@ -169,7 +185,7 @@ export function apply(ctx) {
169
185
  datetime: {
170
186
  type: 'string',
171
187
  description:
172
- '公历本地时间,如 1990-05-12T14:30。bazi / ziwei 必填;liuyao 可选,仅用于取日辰定六神。almanac / star / numerology 只需要日期;mbti 不用时间。;chenggu 需要带时分或用 hour 补时辰;xiaoliuren 时间起课用它的日期与时分。',
188
+ '公历本地时间,如 1990-05-12T14:30。bazi / ziwei 必填;liuyao 可选,仅用于取日辰定六神。almanac / star / numerology 只需要日期;mbti 不用时间。chenggu 需要带时分,或用 hour 补时辰;xiaoliuren 时间起课用它的日期与时分。',
173
189
  },
174
190
  date: {
175
191
  type: 'string',
@@ -248,8 +264,9 @@ export function apply(ctx) {
248
264
  },
249
265
  mode: {
250
266
  type: 'string',
251
- enum: ['solar2lunar', 'lunar2solar'],
252
- description: '仅 convert 使用:转换方向,默认 solar2lunar(公历转农历);lunar2solar 需同时给 lunarYear/lunarMonth/lunarDay。',
267
+ enum: ['solar2lunar', 'lunar2solar', 'time', 'number'],
268
+ description:
269
+ '方向/起法开关,取值按体系分三组,互不重叠。convert(公农历互转):solar2lunar 公历转农历(默认)、lunar2solar 农历转公历(须同时给 lunarYear/lunarMonth/lunarDay)。meihua(梅花易数):time 时间起卦(需 datetime,默认)、number 数字起卦(需 upper/lower)。xiaoliuren(小六壬):time 时间起课(默认)、number 报数起课(需 num1–num3)。',
253
270
  },
254
271
  lunarYear: { type: 'integer', description: '仅 convert 的 lunar2solar:农历年份,如 2026。' },
255
272
  lunarMonth: { type: 'integer', description: '仅 convert 的 lunar2solar:农历月份 1–12。' },
@@ -267,6 +284,10 @@ export function apply(ctx) {
267
284
  type: 'boolean',
268
285
  description: '八字 / 紫微专用:为同一份出生信息再排另一套盘,返回「八字 × 紫微 交叉验证」(同向 / 有张力 / 互补),不合并数值。',
269
286
  },
287
+ hourUnknown: {
288
+ type: 'boolean',
289
+ description: '出生时刻未知(仅 bazi / trend):八字只排年/月/日三柱,时柱不排并标注「时柱暂缺」,摘要首行写「时柱按正午暂算,涉及时柱的结论仅供参考」,起运与交运按日估算;紫微与合盘会以 HOUR_UNKNOWN 阻断。默认 false。',
290
+ },
270
291
  diff: {
271
292
  type: 'boolean',
272
293
  description: '口径对照(bazi / ziwei / qimen):同一组输入分别按当前口径与另一档各排一次,列出根因字段(如日柱、命宫、局数)与连带变化数。只列差异,不判哪套口径更准,也不改盘面。',
@@ -302,10 +323,16 @@ export function apply(ctx) {
302
323
  ask: {
303
324
  type: 'string',
304
325
  description: '追问:围绕已算好的盘回答「某一年 / 某方面」的问题,如「2027 年事业怎么样」「感情如何」。只取盘面已有字段,不重排、不猜测。',
305
- }, withInterpret: {
326
+ },
327
+ withInterpret: {
306
328
  type: 'boolean',
307
329
  description: '是否附带「解析与详解」(五个维度 + 用神/格局等分节 + 免责声明);默认 false,避免默认输出过长。',
308
330
  },
331
+ detail: {
332
+ type: 'string',
333
+ enum: ['full', 'lean'],
334
+ description: '解析文本的档位,仅配合 withInterpret。full(默认)给全部论述与规则出处;lean 只给结论、所问方向那一条与三张表,长论述和出处折成一行指针(省约一半文本)。先拿结论用 lean,用户追问「为什么 / 依据是什么」再换 full;盘面与句柄不受它影响。',
335
+ },
309
336
  asOf: {
310
337
  type: 'string',
311
338
  description: '基准时刻,形如 2026-10-22T16:00。紫微用它定流月/流日/流时;八字用它钉死「流年从哪年起排」;塔罗用它记录 castAt(抽牌时刻)。省略则按调用时的本机当前时刻——要复现某一次结果就显式给出。',
@@ -346,11 +373,6 @@ export function apply(ctx) {
346
373
  },
347
374
  required: ['datetime', 'gender'],
348
375
  },
349
- mode: {
350
- type: 'string',
351
- enum: ['time', 'number'],
352
- description: '梅花易数起卦方式:time 时间起卦(需 datetime),number 数字起卦(需 upper/lower)。默认 time。',
353
- },
354
376
  upper: { type: 'number', description: '梅花数字起卦:第一个数(定上卦)。' },
355
377
  lower: { type: 'number', description: '梅花数字起卦:第二个数(定下卦)。' },
356
378
  changing: { type: 'number', description: '梅花数字起卦:可手动指定动爻(1-6);省略则按两数之和取。' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-metaphysics",
3
- "version": "0.59.0",
3
+ "version": "0.60.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": [
@@ -67,7 +67,7 @@
67
67
  "build:verification": "node tools/verification.mjs",
68
68
  "build:citations": "node tools/citations.mjs",
69
69
  "test": "node tools/test-all.mjs",
70
- "test:ci": "node tools/build-client.mjs && node tools/check-bundle.mjs && node tools/test-all.mjs && node tools/check-assertions.mjs && node tools/verify-tests-can-fail.mjs && node tools/check-html-sinks.mjs && node tools/check-patterns.mjs && node tools/check-perf.mjs",
70
+ "test:ci": "node tools/build-client.mjs && node tools/check-bundle.mjs && node tools/check-verification.mjs && node tools/test-all.mjs && node tools/check-assertions.mjs && node tools/verify-tests-can-fail.mjs && node tools/check-html-sinks.mjs && node tools/check-patterns.mjs && node tools/check-perf.mjs",
71
71
  "test:baseline": "node tools/check-assertions.mjs --update",
72
72
  "bench": "node tools/bench.mjs",
73
73
  "check:html": "node tools/check-html-sinks.mjs",
@@ -79,7 +79,13 @@
79
79
  "release": "node tools/release.mjs",
80
80
  "push:tag": "node tools/push-tag.mjs",
81
81
  "rollback:plan": "node tools/release-rollback.mjs",
82
- "prompt:event": "node tools/event-prompt.mjs"
82
+ "prompt:event": "node tools/event-prompt.mjs",
83
+ "smoke": "node tools/smoke-all.mjs",
84
+ "e2e": "node tools/e2e-dom.mjs && node tools/e2e-panel.mjs",
85
+ "stress": "node test/stress.test.mjs",
86
+ "e2e:page": "node tools/e2e-dom.mjs",
87
+ "e2e:panel": "node tools/e2e-panel.mjs",
88
+ "check:verification": "node tools/check-verification.mjs"
83
89
  },
84
90
  "exports": {
85
91
  ".": "./index.js",
@@ -115,6 +121,9 @@
115
121
  }
116
122
  },
117
123
  "devDependencies": {
118
- "esbuild": "^0.28.2"
124
+ "esbuild": "^0.28.2",
125
+ "playwright-core": "^1.49.1",
126
+ "react": "^18.3.1",
127
+ "react-dom": "^18.3.1"
119
128
  }
120
129
  }