dsh-metaphysics 0.22.0 → 0.38.2

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/mbti.js ADDED
@@ -0,0 +1,417 @@
1
+ // 十六型人格问卷(MBTI 风格):四维度各 6 题,李克特 1–5 计分,按维度净分定字母。
2
+ // 说明:这是「MBTI 风格」的问卷式测算,不是官方 MBTI 量表(未使用其题目、常模与评鉴许可),
3
+ // 也不适合用来做招聘、晋升、诊断等对人的判定。纯本地确定性计算,同样的问题给出同样的结果。
4
+ import { fail } from './fail.js';
5
+
6
+ /** 四维度:两位字母、白话说明、平手时偏向哪一侧。 */
7
+ export const DIMENSIONS = [
8
+ {
9
+ key: 'EI',
10
+ left: 'E',
11
+ right: 'I',
12
+ name: '能量方向',
13
+ leftSay: '向外:和人打交道时来电,边说边想',
14
+ rightSay: '向内:独处时回血,先想清楚再开口',
15
+ tie: 'I',
16
+ note: '不是「会不会社交」,而是社交之后你更有电还是更耗电。',
17
+ },
18
+ {
19
+ key: 'SN',
20
+ left: 'S',
21
+ right: 'N',
22
+ name: '信息接收',
23
+ leftSay: '具体:先看事实、细节和已有经验',
24
+ rightSay: '联想:先看模式、隐喻和下一步可能',
25
+ tie: 'N',
26
+ note: '不是「聪明与否」,而是你先抓住哪一头。',
27
+ },
28
+ {
29
+ key: 'TF',
30
+ left: 'T',
31
+ right: 'F',
32
+ name: '决策依据',
33
+ leftSay: '逻辑:先看是否站得住、是否一致',
34
+ rightSay: '价值:先看人在其中会不会被伤到',
35
+ tie: 'F',
36
+ note: '不是「冷血或情绪化」,而是权衡时先放哪把尺子。',
37
+ },
38
+ {
39
+ key: 'JP',
40
+ left: 'J',
41
+ right: 'P',
42
+ name: '生活节奏',
43
+ leftSay: '收束:先定计划,喜欢事情有个了结',
44
+ rightSay: '开放:先留着口子,喜欢事情还能变',
45
+ tie: 'P',
46
+ note: '不是「勤快或懒散」,而是面对不确定时更愿意钉死还是留着。',
47
+ },
48
+ ];
49
+
50
+ /**
51
+ * 题目:每条只问一件事,答「像我」加分给 side 一侧。
52
+ * 24 题 = 四维度 × 6;顺序交错,避免连着答同一维度而顺着惯性答。
53
+ */
54
+ export const QUESTIONS = [
55
+ { id: 'e1', dim: 'EI', side: 'E', text: '在一个都是陌生人的场合,你会主动找几个人聊起来。' },
56
+ { id: 's1', dim: 'SN', side: 'S', text: '别人讲一件事,你更关心具体发生了什么、细节对不对得上。' },
57
+ { id: 't1', dim: 'TF', side: 'T', text: '朋友来问你两难的选择,你会先把利弊摆清楚再谈感受。' },
58
+ { id: 'j1', dim: 'JP', side: 'J', text: '出门前你习惯把行程和要带的东西先定下来。' },
59
+ { id: 'e2', dim: 'EI', side: 'I', text: '忙完一整天,你更想一个人安静待着,而不是再约一轮。' },
60
+ { id: 's2', dim: 'SN', side: 'N', text: '你常从一个现象跳到「再往下可能还会怎样」的联想上。' },
61
+ { id: 't2', dim: 'TF', side: 'F', text: '做决定时,你更在意这件事对身边人的实际影响。' },
62
+ { id: 'j2', dim: 'JP', side: 'P', text: '计划临时被打乱,你多半能顺势改,不太难受。' },
63
+ { id: 'e3', dim: 'EI', side: 'E', text: '想事情的时候,你更习惯说给人听,说着说着就清楚了。' },
64
+ { id: 's3', dim: 'SN', side: 'S', text: '你更信「做过、验证过」的办法,而不是听起来很新的办法。' },
65
+ { id: 't3', dim: 'TF', side: 'T', text: '规则一旦定了,哪怕对自己不方便,你也倾向照着走。' },
66
+ { id: 'j3', dim: 'JP', side: 'J', text: '有一件事没做完,你心里会一直悬着,想先把它收尾。' },
67
+ { id: 'e4', dim: 'EI', side: 'I', text: '电话响了或有人临时找来,你第一反应是先缓一下再应对。' },
68
+ { id: 's4', dim: 'SN', side: 'N', text: '你容易被「这背后是什么道理」这类问题勾住。' },
69
+ { id: 't4', dim: 'TF', side: 'F', text: '别人情绪不好时,你直觉是先接住情绪,而不是先讲道理。' },
70
+ { id: 'j4', dim: 'JP', side: 'P', text: '你常常边走边看,事到临头再定具体怎么做。' },
71
+ { id: 'e5', dim: 'EI', side: 'E', text: '人多的场合待久了,你通常越待越有精神。' },
72
+ { id: 's5', dim: 'SN', side: 'S', text: '你记事情偏向记「原话、原样、当时的情形」。' },
73
+ { id: 't5', dim: 'TF', side: 'T', text: '和人争论时,你更在意谁的推理有漏洞,而不是谁伤了面子。' },
74
+ { id: 'j5', dim: 'JP', side: 'J', text: '你喜欢把待办列出来,一条条划掉。' },
75
+ { id: 'e6', dim: 'EI', side: 'I', text: '有新想法时,你更愿意先自己消化一轮,再拿出去讲。' },
76
+ { id: 's6', dim: 'SN', side: 'N', text: '比起照现状做好,你更享受把现状改成一个更好的样子。' },
77
+ { id: 't6', dim: 'TF', side: 'F', text: '要做取舍时,你会想「这样是不是对人不公平」。' },
78
+ { id: 'j6', dim: 'JP', side: 'P', text: '临出发才收拾行李,你也觉得没什么问题。' },
79
+ ];
80
+
81
+ /** 认知功能栈(主导 → 劣势),供类型详情里展开。 */
82
+ export const FUNCTION_STACKS = {
83
+ ISTJ: ['Si', 'Te', 'Fi', 'Ne'],
84
+ ISFJ: ['Si', 'Fe', 'Ti', 'Ne'],
85
+ INFJ: ['Ni', 'Fe', 'Ti', 'Se'],
86
+ INTJ: ['Ni', 'Te', 'Fi', 'Se'],
87
+ ISTP: ['Ti', 'Se', 'Ni', 'Fe'],
88
+ ISFP: ['Fi', 'Se', 'Ni', 'Te'],
89
+ INFP: ['Fi', 'Ne', 'Si', 'Te'],
90
+ INTP: ['Ti', 'Ne', 'Si', 'Fe'],
91
+ ESTP: ['Se', 'Ti', 'Fe', 'Ni'],
92
+ ESFP: ['Se', 'Fi', 'Te', 'Ni'],
93
+ ENFP: ['Ne', 'Fi', 'Te', 'Si'],
94
+ ENTP: ['Ne', 'Ti', 'Fe', 'Si'],
95
+ ESTJ: ['Te', 'Si', 'Ne', 'Fi'],
96
+ ESFJ: ['Fe', 'Si', 'Ne', 'Ti'],
97
+ ENFJ: ['Fe', 'Ni', 'Se', 'Ti'],
98
+ ENTJ: ['Te', 'Ni', 'Se', 'Fi'],
99
+ };
100
+
101
+ /** 八个认知功能的白话一句话。 */
102
+ export const FUNCTIONS = {
103
+ Si: '内感觉:拿过去的经验当尺子,稳、细、记得住。',
104
+ Se: '外感觉:对眼前的具体变化反应快,先动手再说。',
105
+ Ni: '内直觉:把零碎线索收成一个方向感,先看到终局。',
106
+ Ne: '外直觉:从一个点炸开很多可能,擅长另起一条路。',
107
+ Ti: '内思考:要自己把逻辑盘通,讲得出为什么才认。',
108
+ Te: '外思考:要事情跑起来,讲流程、指标和结果。',
109
+ Fi: '内情感:先问「这符不符合我在意的东西」,价值感很强。',
110
+ Fe: '外情感:先读空气、先顾关系,擅长把人和在一起。',
111
+ };
112
+
113
+ /**
114
+ * 十六型:每型给白话称号 + 三句倾向 + 优势 + 课题 + 工作/关系/学习各一句。
115
+ * 措辞只作参照,不断定个人能力与命运。
116
+ */
117
+ export const TYPES = {
118
+ ISTJ: {
119
+ title: '检查者',
120
+ keywords: '稳、准、守得住',
121
+ oneLine: '答应的事会做完,讨厌临时改口和半成品。',
122
+ strengths: ['流程感强,细节不漏', '说到做到,扛得住长期重复', '遇到混乱先立规矩'],
123
+ blindspots: ['对「新办法」先起疑,容易错过转弯的机会', '把责任揽太多,难受也说不出口'],
124
+ work: '适合规则清楚、可累积经验的岗位:审计、运维、法务、工程管理。',
125
+ love: '表达朴素但稳定,重视承诺;需要对方把需求说明白,别指望你自己猜。',
126
+ study: '靠体系与刷题推进,把知识整理成清单会明显提速。',
127
+ growth: '练一件「没有标准答案」的事,容许自己在过程中改主意。',
128
+ },
129
+ ISFJ: {
130
+ title: '守护者',
131
+ keywords: '暖、细、记得住人',
132
+ oneLine: '会默默把别人的需要记在心里,然后提前做好。',
133
+ strengths: ['照顾细节与情绪', '记性好,了解身边人的习惯', '愿意做没人愿意做的收尾'],
134
+ blindspots: ['习惯先委屈自己,之后积成怨', '对方没察觉你的付出时容易失落'],
135
+ work: '适合需要耐心与责任心的岗位:护理、教育、行政、客户成功、人事。',
136
+ love: '用做事表达感情,需要被明确看见与感谢;也要练习直接要。',
137
+ study: '喜欢有例题、有反馈的学习方式,做学习小组的稳定输出者很合适。',
138
+ growth: '练习说「这件事我做不到」,先照顾自己的电量。',
139
+ },
140
+ INFJ: {
141
+ title: '提倡者',
142
+ keywords: '深、理想、看穿人心',
143
+ oneLine: '关心人为什么这么活,愿意为一个「值得」的目标长期投入。',
144
+ strengths: ['能从细节里读出动机', '表达有感染力,适合讲清意义', '对长期方向有耐心'],
145
+ blindspots: ['标准高,容易对现实失望', '想得太深而迟迟不开口,外人看不出你在乎'],
146
+ work: '适合内容、咨询、教育、公益、产品策略这类「讲清一件事」的岗位。',
147
+ love: '要的是深谈与理解,浅社交会耗电;需要对方给得起精神上的回应。',
148
+ study: '适合主题式深挖,写读书笔记与建立自己的框架比刷题更有效。',
149
+ growth: '把理想拆成这周能交付的一小步,别只在脑子里完成它。',
150
+ },
151
+ INTJ: {
152
+ title: '建筑师',
153
+ keywords: '远、冷静、自己搭系统',
154
+ oneLine: '先看清全局再动手,讨厌无效沟通和低效流程。',
155
+ strengths: ['长期规划清晰', '能独立啃下复杂问题', '对标准挑剔,产品因此更硬'],
156
+ blindspots: ['说话直,容易让人觉得被否定', '对「慢」和「情绪」耐心不足'],
157
+ work: '适合战略、研发、系统设计、投资研究这类需要独立判断的岗位。',
158
+ love: '表达感情偏克制,用解决问题的方式示好;需要对方接受你的独处需求。',
159
+ study: '自学效率高,适合先建框架再补细节;警惕只读不产出。',
160
+ growth: '在给方案之前先接一句对方的感受,合作阻力会小很多。',
161
+ },
162
+ ISTP: {
163
+ title: '巧匠',
164
+ keywords: '手快、冷静、先修再说',
165
+ oneLine: '遇到问题先上手拆开看,不爱开长会。',
166
+ strengths: ['动手能力强,现场判断快', '压力下不慌,能抓住关键', '讲实际,不绕弯'],
167
+ blindspots: ['对长期计划不感兴趣,容易拖到最后', '话少,别人不知道你在想什么'],
168
+ work: '适合现场性强的岗位:工程、设备、急救、运维、技术支持、手艺型工作。',
169
+ love: '不擅长说,但会用行动帮忙;需要对方接受你「不解释」的习惯。',
170
+ study: '边做边学最快,先碰真实设备或真实项目,再回头补理论。',
171
+ growth: '给重要的事留一个检查点,别全靠临场反应。',
172
+ },
173
+ ISFP: {
174
+ title: '探险家',
175
+ keywords: '真、敏感、跟着感觉走',
176
+ oneLine: '在意「这件事对不对味」,不喜欢被安排得太死。',
177
+ strengths: ['审美与体感准', '待人温和,不强加于人', '在真实情境里学得快'],
178
+ blindspots: ['不愿冲突,把不满压着', '长期目标容易停在「想」这一步'],
179
+ work: '适合与美、身体、具体对象打交道的岗位:设计、摄影、餐饮、康复、手作。',
180
+ love: '用陪伴和小惊喜表达在意,讨厌被审问;需要空间也需要被确认。',
181
+ study: '先看示例、先动手,理论放在用到的时候补,效率更高。',
182
+ growth: '给在意的事定一个可交付的日期,把感觉变成作品。',
183
+ },
184
+ INFP: {
185
+ title: '调停者',
186
+ keywords: '柔、较真、心里有一杆秤',
187
+ oneLine: '对人真诚,对自己在意的事格外较真,讨厌虚伪。',
188
+ strengths: ['共情与想象力都好', '写与表达有个人味道', '能为价值观长期坚持'],
189
+ blindspots: ['容易把「不够好」归到自己身上', '计划性弱,想法多而落地慢'],
190
+ work: '适合创作、教育、心理、内容、公益这类能表达价值的岗位;强规则环境会耗电。',
191
+ love: '看重真诚与被理解,愿意为对方调整;需要明确的回应,别长期猜。',
192
+ study: '靠兴趣驱动,给自己找「为什么学」比逼自己更有效;用短周期输出防拖延。',
193
+ growth: '把心里的标准写下来,分「必须有」和「有更好」,少一点全或无。',
194
+ },
195
+ INTP: {
196
+ title: '逻辑家',
197
+ keywords: '想透、好问、自己盘逻辑',
198
+ oneLine: '喜欢把问题拆到原理层,讨厌「一直这样所以就这样」。',
199
+ strengths: ['抽象思考强,能发现系统里的漏洞', '学新领域快', '不轻易被权威说服'],
200
+ blindspots: ['想得多做得少,收尾困难', '指出问题时不修饰,容易树敌'],
201
+ work: '适合研究、算法、架构、分析、写作这类需要反复推演的岗位。',
202
+ love: '用分享想法表达亲近,需要对方能聊得深;别让讨论变成辩论。',
203
+ study: '先弄清原理再练,适合自己搭知识树;给自己设「讲给别人听」的交付点。',
204
+ growth: '给探索设时间盒,到点就交出一个能用的版本。',
205
+ },
206
+ ESTP: {
207
+ title: '企业家',
208
+ keywords: '快、敢、临场能打',
209
+ oneLine: '看到机会先动手,比开会讨论更信现场反馈。',
210
+ strengths: ['行动快,敢试错', '会看人、会周旋', '压力下反而清醒'],
211
+ blindspots: ['耐心不足,容易留一堆半成品', '对长期代价估计偏轻'],
212
+ work: '适合销售、商务、创业、现场运营、竞技类岗位;文牍型工作会闷。',
213
+ love: '热烈直接,喜欢一起做事;需要对方接受你「说到就到」的节奏。',
214
+ study: '靠实战与竞赛驱动,做项目比听课有效;需要有人帮你收尾。',
215
+ growth: '动手前多问一句「三个月后这件事还成立吗」。',
216
+ },
217
+ ESFP: {
218
+ title: '表演者',
219
+ keywords: '热、会带气氛、活在当下',
220
+ oneLine: '在人群里自带气氛,喜欢真实可感的体验。',
221
+ strengths: ['感染力强,能把场子撑起来', '对人和现场的细节敏感', '愿意尝试新鲜事'],
222
+ blindspots: ['对长期枯燥的事耐力有限', '不太愿意面对冲突背后的结构问题'],
223
+ work: '适合面向人的岗位:活动、销售、主持、文旅、服务、内容出镜。',
224
+ love: '表达直接,喜欢一起玩;需要对方给你稳定感,也别把自由当借口。',
225
+ study: '边玩边学最有效,把知识做成游戏、演示或小组活动。',
226
+ growth: '留一件「不好玩但重要」的事,按周推进。',
227
+ },
228
+ ENFP: {
229
+ title: '竞选者',
230
+ keywords: '亮、点子多、对人好奇',
231
+ oneLine: '对人和可能性都很好奇,容易同时点亮很多盏灯。',
232
+ strengths: ['联想快,能给人新视角', '真诚热情,容易建立信任', '擅长起头与鼓舞'],
233
+ blindspots: ['兴趣转移快,收尾吃力', '情绪起伏大,容易被细节拖垮'],
234
+ work: '适合创意、品牌、内容、培训、早期项目这类需要不断开新局的地方。',
235
+ love: '需要新鲜感与被看见,愿意为对方想很多;要练习把承诺落到实处。',
236
+ study: '用主题和讨论驱动,找学习搭子比一个人硬啃有效。',
237
+ growth: '同时只开三条线,其余写进「以后再说」清单。',
238
+ },
239
+ ENTP: {
240
+ title: '辩论家',
241
+ keywords: '机敏、爱拆、想法往外冒',
242
+ oneLine: '喜欢把既有说法掰开看,享受把问题换个角度。',
243
+ strengths: ['反应快,能现场找到漏洞', '对新领域上手快', '擅长把复杂事讲得好玩'],
244
+ blindspots: ['为辩而辩,容易伤人', '起头多、收尾少'],
245
+ work: '适合咨询、产品、创业、策略、媒体这类需要不断破局的地方。',
246
+ love: '用斗嘴表达亲近,需要对方接得住;别把关系也当成辩论场。',
247
+ study: '讨论与质疑中理解最快,适合做分享者;给自己定交付时间。',
248
+ growth: '挑一件「不新但重要」的事,把它做完再开下一个。',
249
+ },
250
+ ESTJ: {
251
+ title: '总经理',
252
+ keywords: '务实、能扛、把事推到底',
253
+ oneLine: '目标定了就拆任务、定人、定时间,讨厌含糊。',
254
+ strengths: ['组织与推进力强', '标准清楚,团队知道该做什么', '对结果负责'],
255
+ blindspots: ['节奏太紧,忽视人的感受', '对新方法的容忍度低'],
256
+ work: '适合管理、运营、项目、供应链、政务这类要「把事办成」的岗位。',
257
+ love: '用承担责任表达在意,需要对方直接说需求;记得留不安排的时间。',
258
+ study: '目标拆解 + 打卡推进最有效,适合带小组复习。',
259
+ growth: '在下判断前多问一句「有没有我没想到的难处」。',
260
+ },
261
+ ESFJ: {
262
+ title: '执政官',
263
+ keywords: '周到、热络、把人放在心上',
264
+ oneLine: '习惯先照顾气氛和关系,让事情在人情里办成。',
265
+ strengths: ['组织人际与活动', '记得住每个人的状况', '愿意为集体做脏活累活'],
266
+ blindspots: ['太在意别人评价,容易内耗', '对「不合群」的选项排斥'],
267
+ work: '适合人力、行政、教育、客户、医护、社群运营这类以人为中心的岗位。',
268
+ love: '投入、周到,需要明确的回应与稳定;别把照顾变成控制。',
269
+ study: '在小组里学得最好,讲给别人听会记得更牢。',
270
+ growth: '练习在不解释的情况下做一次自己的选择。',
271
+ },
272
+ ENFJ: {
273
+ title: '主人公',
274
+ keywords: '暖、会带人、看得见潜力',
275
+ oneLine: '天然想把人和事往好的方向推,愿意为别人多走一步。',
276
+ strengths: ['洞察他人需求', '善于组织与鼓舞', '把长期关系经营得住'],
277
+ blindspots: ['过度承担别人的情绪', '为了和谐压下自己的判断'],
278
+ work: '适合教育、管理、培训、公益、品牌这类「带人成长」的岗位。',
279
+ love: '投入且会照顾,需要对方也主动;别把付出当成默认义务。',
280
+ study: '教别人是最好的学法,适合做学习小组的组织者。',
281
+ growth: '把「我该帮」和「不该我扛」分开,先照顾自己的电量。',
282
+ },
283
+ ENTJ: {
284
+ title: '指挥官',
285
+ keywords: '决断、看清、把盘做大',
286
+ oneLine: '看得远也敢拍板,喜欢把资源调到最该去的地方。',
287
+ strengths: ['目标与路径清楚', '能在压力下决断', '擅长组织复杂局面'],
288
+ blindspots: ['节奏压人,忽略情绪成本', '把「效率」当成唯一标准'],
289
+ work: '适合管理、创业、投资、战略、大型项目这类要定方向并扛结果的岗位。',
290
+ love: '用规划和承担表达在意,需要对方能对话;记得把「安排」换成「商量」。',
291
+ study: '先建目标与评估标准,按里程碑推进;警惕只搭框架不落细节。',
292
+ growth: '定期问自己「这件事赢了,人会怎样」,把人的成本算进去。',
293
+ },
294
+ };
295
+
296
+ const SCALE_SAY = {
297
+ 1: '完全不像',
298
+ 2: '不太像',
299
+ 3: '说不准',
300
+ 4: '有点像',
301
+ 5: '很像我',
302
+ };
303
+
304
+ /** 供面板/工具引导用户答题时展示的问卷(不带答案)。 */
305
+ export function mbtiPaper() {
306
+ return {
307
+ system: 'mbti',
308
+ needAnswers: true,
309
+ scale: SCALE_SAY,
310
+ total: QUESTIONS.length,
311
+ dimensions: DIMENSIONS.map((d) => ({ key: d.key, name: d.name, left: d.left, right: d.right, leftSay: d.leftSay, rightSay: d.rightSay, note: d.note })),
312
+ questions: QUESTIONS.map((q, i) => ({ no: i + 1, id: q.id, dim: q.dim, text: q.text })),
313
+ note: '这是一份 MBTI 风格的问卷(不是官方 MBTI 量表):每题按 1(完全不像我)到 5(很像我)打分,把 24 个分数按顺序交给 answers 即可出类型。题目只作自我参照,不适合用来评价别人的能力或做招聘、晋升等决定。',
314
+ };
315
+ }
316
+
317
+ /** 把 answers 归一成 24 个数(允许少于 24 题,缺的按「不表态」处理;null/0 也算跳过)。 */
318
+ function normalizeAnswers(answers) {
319
+ if (!Array.isArray(answers)) {
320
+ throw fail('E_INPUT', 'answers 必须是数组:每题 1–5 分,按 questions 的顺序给;答题前先不带 answers 调一次,拿到题目');
321
+ }
322
+ if (answers.length > QUESTIONS.length) {
323
+ throw fail('E_INPUT', `answers 最多 ${QUESTIONS.length} 个(对应 ${QUESTIONS.length} 题),收到 ${answers.length} 个`);
324
+ }
325
+ const out = [];
326
+ for (let i = 0; i < QUESTIONS.length; i += 1) {
327
+ const raw = answers[i];
328
+ if (raw === undefined || raw === null || raw === '') {
329
+ out.push(null);
330
+ continue;
331
+ }
332
+ const v = Number(raw);
333
+ if (!Number.isInteger(v) || v < 1 || v > 5) {
334
+ throw fail('E_RANGE', `第 ${i + 1} 题的分值必须是 1–5 的整数(1 完全不像,5 很像我),收到「${raw}」`);
335
+ }
336
+ out.push(v);
337
+ }
338
+ return out;
339
+ }
340
+
341
+ /** 净分 → 百分比与强度描述。 */
342
+ function strengthOf(pct, answered) {
343
+ if (!answered) return '未作答';
344
+ if (pct <= 0.12) return '临界';
345
+ if (pct <= 0.3) return '轻微';
346
+ if (pct <= 0.6) return '明显';
347
+ return '很强';
348
+ }
349
+
350
+ /**
351
+ * 计分:每题的净分 =(分数 − 3)× 侧向符号(side 为左字母取 +,右字母取 −)。
352
+ * 维度净分 > 0 取左字母,< 0 取右字母,= 0 按维度表的 tie 取(并标「临界」)。
353
+ */
354
+ export function buildMbti(input = {}) {
355
+ const answers = normalizeAnswers(input.answers);
356
+ const answeredAll = answers.filter((v) => v !== null).length;
357
+ if (answeredAll < 12) {
358
+ throw fail('E_INPUT', `至少答 ${12} 题才能出口径(现答 ${answeredAll} 题);可以先不带 answers 调一次,把题目发给用户逐题打分`);
359
+ }
360
+ const dims = DIMENSIONS.map((d) => {
361
+ let sum = 0;
362
+ let answered = 0;
363
+ QUESTIONS.forEach((q, i) => {
364
+ const v = answers[i];
365
+ if (v === null || q.dim !== d.key) return;
366
+ answered += 1;
367
+ sum += (v - 3) * (q.side === d.left ? 1 : -1);
368
+ });
369
+ const maxAbs = answered * 2;
370
+ const pct = maxAbs ? Math.abs(sum) / maxAbs : 0;
371
+ const picked = sum > 0 ? d.left : sum < 0 ? d.right : d.tie;
372
+ return {
373
+ key: d.key,
374
+ name: d.name,
375
+ left: d.left,
376
+ right: d.right,
377
+ leftSay: d.leftSay,
378
+ rightSay: d.rightSay,
379
+ note: d.note,
380
+ sum,
381
+ answered,
382
+ maxAbs,
383
+ pct: Math.round(pct * 1000) / 1000,
384
+ picked,
385
+ strength: strengthOf(pct, answered),
386
+ tie: sum === 0,
387
+ };
388
+ });
389
+ const type = dims.map((d) => d.picked).join('');
390
+ const profile = TYPES[type];
391
+ if (!profile) throw fail('E_SYSTEM', `内部错误:类型 ${type} 没有对应文案`);
392
+ const stack = (FUNCTION_STACKS[type] || []).map((f) => ({ code: f, say: FUNCTIONS[f] || '' }));
393
+ return {
394
+ system: 'mbti',
395
+ type,
396
+ title: profile.title,
397
+ keywords: profile.keywords,
398
+ oneLine: profile.oneLine,
399
+ dims: dims.map(({ sum, maxAbs, answered, ...rest }) => ({ ...rest, sum, maxAbs, answered })),
400
+ profile: {
401
+ title: profile.title,
402
+ keywords: profile.keywords,
403
+ oneLine: profile.oneLine,
404
+ strengths: profile.strengths.slice(),
405
+ blindspots: profile.blindspots.slice(),
406
+ work: profile.work,
407
+ love: profile.love,
408
+ study: profile.study,
409
+ growth: profile.growth,
410
+ stack,
411
+ },
412
+ answered: answeredAll,
413
+ skipped: QUESTIONS.length - answeredAll,
414
+ total: QUESTIONS.length,
415
+ note: '四维各 6 题、李克特 1–5 计分,按维度净分取字母(平手时取 I / N / F / P 一侧并标「临界」)。这是 MBTI 风格的问卷测算,不是官方量表;类型只作自我参照,不是能力评价,也不用它给人下结论。',
416
+ };
417
+ }
@@ -33,6 +33,19 @@ const YONG_SHEN = {
33
33
  travel: { label: '出行', primary: ['九天'], secondary: ['驿马'], note: '出行以九天为主,兼看驿马' },
34
34
  };
35
35
 
36
+ /** 天盘干与宫的落宫关系统计:干克宫 3 宫(辛)… */
37
+ function stemRelationLine(r) {
38
+ const groups = new Map();
39
+ for (const p of r.palaces || []) {
40
+ const rel = p.stemRelation || null;
41
+ if (!rel || rel.type === 'unknown' || !p.tianPan) continue;
42
+ if (!groups.has(rel.type)) groups.set(rel.type, []);
43
+ groups.get(rel.type).push(`${p.palace}宫${p.tianPan}`);
44
+ }
45
+ const order = ['干克宫', '宫克干', '干生宫', '宫生干', '比和'];
46
+ return order.filter((k) => groups.has(k)).map((k) => `${k} ${groups.get(k).join('、')}`).join(';') || '—';
47
+ }
48
+
36
49
  const REL_TEXT = {
37
50
  生: '主体力量流向环境或趋势',
38
51
  克: '主体对环境或趋势构成压力、有改造意图',
@@ -128,15 +141,37 @@ export function interpretQimen(r, opts = {}) {
128
141
  else if (rel.type === '宫制门') controlled.push(`${p.palace}宫(宫${rel.palace}克${p.gate}${rel.gate})`);
129
142
  }
130
143
  const kongCells = r.palaces.filter((p) => p.kong).map((p) => `${p.palace}宫`);
144
+ const jiXing = (r.conditions && r.conditions.jiXing) || [];
145
+ const ruMu = (r.conditions && r.conditions.ruMu) || [];
131
146
  sections.push({
132
147
  key: 'conditions',
133
148
  label: '宫位条件',
134
149
  text: `门迫(同宫内门克宫):${forced.length ? forced.join('、') : '本盘无'};`
135
150
  + `宫制门(宫克门,属「制」不为迫):${controlled.length ? controlled.join('、') : '无'}。`
151
+ + `击刑(天盘六仪落刑宫):${jiXing.length ? jiXing.map((x) => `${x.palace}宫${x.stem}(${x.basis})`).join('、') : '本盘无'};`
152
+ + `入墓(天盘三奇落墓宫):${ruMu.length ? ruMu.map((x) => `${x.palace}宫${x.stem}(${x.basis})`).join('、') : '本盘无'}。`
136
153
  + (kongCells.length ? `空亡:${kongCells.join('、')}——落空之宫事多不实。` : '无落空之宫。')
137
- + `门迫只作宫位条件,不自动等同于用神受制。`,
154
+ + `这三项都只作宫位条件:门迫、击刑、入墓不自动等同于用神受制,取象时看它们落在哪个符号上。`,
138
155
  });
139
156
 
157
+ // ⑤b 月令旺衰:门、星、天盘干按月支五行判五态(与门迫这类宫位关系分开)
158
+ const mo = r.month || null;
159
+ const st = r.states || null;
160
+ if (mo && st) {
161
+ const order = ['旺', '相', '休', '囚', '死'];
162
+ const line = (g) => order.filter((k) => g && Array.isArray(g[k]) && g[k].length)
163
+ .map((k) => `${k} ${g[k].join('、')}`).join(';') || '—';
164
+ sections.push({
165
+ key: 'states',
166
+ label: '月令旺衰',
167
+ text: `月令 ${mo.branch}(${mo.element}):`
168
+ + `门 ${line(st.gate)}。星 ${line(st.star)}。天盘干 ${line(st.stem)}。`
169
+ + `旺为同月令之气,相为月令所生,休为生月令,囚为克月令,死为月令所克。`
170
+ + `干宫的落宫关系另算:${stemRelationLine(r)}。`
171
+ + `这一层只说明门、星、干自身力量强弱,不合吉凶:门旺而宫克门,仍受宫制;星旺而落宫死地,也难全其用。`,
172
+ });
173
+ }
174
+
140
175
  // ⑥ 条件式倾向
141
176
  if (unresolved.length) {
142
177
  sections.push({
@@ -151,7 +186,7 @@ export function interpretQimen(r, opts = {}) {
151
186
  key: 'tendency',
152
187
  label: '最终倾向',
153
188
  text: `在当前输入、规则集与所问之事下:若以「${primary.key}」为用神,用神宫对值符宫为「${rel}」,整体${tone};`
154
- + `若${ys.label}实际取另一个候选,或时间范围不同,解读边界随之变化。未决项:月令旺衰未纳入本规则集。`,
189
+ + `若${ys.label}实际取另一个候选,或时间范围不同,解读边界随之变化。未决项:天盘干与地盘的十干克应(100 组)未纳入本规则集,干宫生克只作落宫条件。`,
155
190
  });
156
191
  }
157
192
 
package/engine/qimen.js CHANGED
@@ -1,4 +1,4 @@
1
- // 时家奇门(转盘,拆补法定局)。
1
+ // 时家奇门(转盘,拆补法定局)。
2
2
  // 算法移植自 MIT 许可的 qimendunjia-standalone@0.1.0(作者:见其 LICENSE),
3
3
  // 保留其定局/排盘口径(符头定三元、值符值使推演、天盘随值符旋转、中五宫寄坤二),
4
4
  // 只把历法层换成本插件自己的 calendar/lunar 模块——节气、干支、农历都走同一套已验证的源码。
@@ -179,6 +179,108 @@ export function gatePalaceRelation(gate, palace) {
179
179
  if (SHENG[pe] === ge) return { type: '宫生门', gate: ge, palace: pe, note: `${pe}生${ge}` };
180
180
  return { type: '比和', gate: ge, palace: pe, note: `${ge}与${pe}比和` };
181
181
  }
182
+
183
+ // ── 天盘干:五行、与所落宫的生克 ─────────────────────────────
184
+ // 天盘干(三奇六仪,甲隐于六仪,盘面上不出现甲)与所落宫五行的生克,
185
+ // 是与门宫关系并列的一层「干—宫」关系,只作落宫条件,不折吉凶。
186
+ const STEM_ELEMENT = {
187
+ 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土',
188
+ 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水',
189
+ };
190
+
191
+ /** 天盘干与其所落宫的关系:干克宫 / 宫克干 / 干生宫 / 宫生干 / 比和 */
192
+ export function stemPalaceRelation(stem, palace) {
193
+ const se = STEM_ELEMENT[stem];
194
+ const pe = PALACE_ELEMENT[palace];
195
+ if (!se || !pe) return { type: 'unknown', stem: se || null, palace: pe || null, note: '缺少干或宫的五行,无法判断' };
196
+ if (KE[se] === pe) return { type: '干克宫', stem: se, palace: pe, note: `${se}克${pe},天盘干克宫` };
197
+ if (KE[pe] === se) return { type: '宫克干', stem: se, palace: pe, note: `${pe}克${se},宫克天盘干` };
198
+ if (SHENG[se] === pe) return { type: '干生宫', stem: se, palace: pe, note: `${se}生${pe},干泄气于宫` };
199
+ if (SHENG[pe] === se) return { type: '宫生干', stem: se, palace: pe, note: `${pe}生${se},宫来生干` };
200
+ return { type: '比和', stem: se, palace: pe, note: `${se}与${pe}比和` };
201
+ }
202
+
203
+ // ── 六仪击刑 / 三奇入墓 ─────────────────────────────────────
204
+ // 两者都是「天盘干落到某个宫就成立」的硬条件,与门迫一样只作宫位条件,不下吉凶。
205
+ // 六仪击刑按地支相刑推:子刑卯、戌刑未、申刑寅、午自刑、辰自刑、寅刑巳;
206
+ // 三奇入墓按五行墓库推:乙木墓未(未在坤二宫)、丙丁火墓戌(戌在乾六宫)。
207
+ const JI_XING = {
208
+ 戊: { palace: '3', zhi: '卯', basis: '甲子戊,子刑卯' },
209
+ 己: { palace: '2', zhi: '未', basis: '甲戌己,戌刑未' },
210
+ 庚: { palace: '8', zhi: '寅', basis: '甲申庚,申刑寅' },
211
+ 辛: { palace: '9', zhi: '午', basis: '甲午辛,午自刑' },
212
+ 壬: { palace: '4', zhi: '辰', basis: '甲辰壬,辰自刑' },
213
+ 癸: { palace: '4', zhi: '巳', basis: '甲寅癸,寅刑巳' },
214
+ };
215
+ const RU_MU = {
216
+ 乙: { palace: '2', zhi: '未', basis: '乙木墓于未,未在坤二宫' },
217
+ 丙: { palace: '6', zhi: '戌', basis: '丙火墓于戌,戌在乾六宫' },
218
+ 丁: { palace: '6', zhi: '戌', basis: '丁火墓于戌,戌在乾六宫' },
219
+ };
220
+
221
+ /** 天盘六仪落刑宫 → [{palace, stem, zhi, basis}] */
222
+ export function jiXingOf(palaces) {
223
+ const out = [];
224
+ for (const p of palaces || []) {
225
+ const hit = JI_XING[p.tianPan];
226
+ if (hit && hit.palace === String(p.palace)) out.push({ palace: p.palace, stem: p.tianPan, zhi: hit.zhi, basis: hit.basis });
227
+ }
228
+ return out;
229
+ }
230
+
231
+ /** 天盘三奇入墓 → [{palace, stem, zhi, basis}] */
232
+ export function ruMuOf(palaces) {
233
+ const out = [];
234
+ for (const p of palaces || []) {
235
+ const hit = RU_MU[p.tianPan];
236
+ if (hit && hit.palace === String(p.palace)) out.push({ palace: p.palace, stem: p.tianPan, zhi: hit.zhi, basis: hit.basis });
237
+ }
238
+ return out;
239
+ }
240
+
241
+ /** 供测试与文档引用:两张表的只读视图 */
242
+ export const QIMEN_CONDITION_TABLES = Object.freeze({
243
+ jiXing: Object.freeze(Object.fromEntries(Object.entries(JI_XING).map(([k, v]) => [k, { ...v }]))),
244
+ ruMu: Object.freeze(Object.fromEntries(Object.entries(RU_MU).map(([k, v]) => [k, { ...v }]))),
245
+ });
246
+
247
+ // ── 月令旺衰:门与星对月令的五态 ─────────────────────────────
248
+ // 五行在月令的旺相休囚死:与月令同气为旺,月令生之为相,生月令为休,
249
+ // 克月令为囚,被月令克为死。只作「力量强弱」的事实,不折成吉凶。
250
+ const BRANCH_ELEMENT = {
251
+ 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
252
+ 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
253
+ };
254
+ /** 旺相休囚死:element 在 monthElement 月令中的状态 */
255
+ export function monthState(element, monthElement) {
256
+ if (!element || !monthElement) return 'unknown';
257
+ if (element === monthElement) return '旺';
258
+ if (SHENG[monthElement] === element) return '相';
259
+ if (SHENG[element] === monthElement) return '休';
260
+ if (KE[element] === monthElement) return '囚';
261
+ if (KE[monthElement] === element) return '死';
262
+ return 'unknown';
263
+ }
264
+ /** 月支 → 月令五行 */
265
+ export function monthElementOf(branch) {
266
+ return BRANCH_ELEMENT[branch] || null;
267
+ }
268
+ /** 把一组 {name, element} 按状态归堆 → {旺:[…],相:[…],休:[…],囚:[…],死:[…]} */
269
+ export function groupByState(items, monthElement) {
270
+ const out = { 旺: [], 相: [], 休: [], 囚: [], 死: [] };
271
+ for (const it of items || []) {
272
+ const st = monthState(it.element, monthElement);
273
+ if (out[st]) out[st].push(it.name);
274
+ }
275
+ return out;
276
+ }
277
+ /** 供测试与文档引用:门/星五行与月支五行的只读视图 */
278
+ export const QIMEN_STATE_TABLES = Object.freeze({
279
+ gate: Object.freeze({ ...GATE_ELEMENT }),
280
+ star: Object.freeze({ ...STAR_ELEMENT }),
281
+ branch: Object.freeze({ ...BRANCH_ELEMENT }),
282
+ stem: Object.freeze({ ...STEM_ELEMENT }),
283
+ });
182
284
  function getDiPan(type, num) {
183
285
  const result = {};
184
286
  let gong = num;
@@ -360,6 +462,10 @@ export function buildQimen(input) {
360
462
  const maZhi = maMap[hourGz[1]] || '';
361
463
  const maGong = ZHI_TO_GONG[maZhi] || '';
362
464
 
465
+ // 月令:门与星的力量强弱都按月支五行判,与局数、宫位分开记
466
+ const monthBranch = (snapshot.monthGanZhi || '')[1] || '';
467
+ const monthElement = BRANCH_ELEMENT[monthBranch] || null;
468
+
363
469
  // 中五宫单列(天禽寄坤二,门神为空),与参照实现的九宫结构对齐
364
470
  const centerPalace = {
365
471
  palace: '5',
@@ -372,7 +478,11 @@ export function buildQimen(input) {
372
478
  palaceElement: PALACE_ELEMENT['5'],
373
479
  gateElement: null,
374
480
  starElement: STAR_ELEMENT['天禽'],
481
+ stemElement: STEM_ELEMENT[tianPan['5']] || null,
375
482
  gateRelation: { type: 'unknown', note: '中宫无门,不参与门宫关系' },
483
+ stemRelation: stemPalaceRelation(tianPan['5'], '5'),
484
+ starState: monthState(STAR_ELEMENT['天禽'], monthElement),
485
+ stemState: monthState(STEM_ELEMENT[tianPan['5']], monthElement),
376
486
  kong: false,
377
487
  ma: false,
378
488
  center: true,
@@ -389,7 +499,12 @@ export function buildQimen(input) {
389
499
  palaceElement: PALACE_ELEMENT[gong],
390
500
  gateElement: GATE_ELEMENT[bm.baMen[gong]] || null,
391
501
  starElement: STAR_ELEMENT[jx.jiuXing[gong]] || null,
502
+ stemElement: STEM_ELEMENT[tianPan[gong]] || null,
392
503
  gateRelation: gatePalaceRelation(bm.baMen[gong], gong),
504
+ stemRelation: stemPalaceRelation(tianPan[gong], gong),
505
+ gateState: monthState(GATE_ELEMENT[bm.baMen[gong]], monthElement),
506
+ starState: monthState(STAR_ELEMENT[jx.jiuXing[gong]], monthElement),
507
+ stemState: monthState(STEM_ELEMENT[tianPan[gong]], monthElement),
393
508
  kong: kongGong.includes(gong),
394
509
  ma: maGong === gong,
395
510
  }));
@@ -418,6 +533,18 @@ export function buildQimen(input) {
418
533
  zhiShi: { gate: bm.zhiShiMen, gong: bm.zhiShiGong },
419
534
  kongWang: kongZhi,
420
535
  ma: { zhi: maZhi, gong: maGong },
536
+ // 宫位硬条件(天盘干定宫):六仪击刑、三奇入墓。只列成立项,不排序、不打分
537
+ conditions: {
538
+ jiXing: jiXingOf(palaces),
539
+ ruMu: ruMuOf(palaces),
540
+ },
541
+ // 月令与门/星旺衰:按月支五行判旺相休囚死,只报强弱、不折吉凶
542
+ month: { branch: monthBranch, element: monthElement },
543
+ states: {
544
+ gate: groupByState(palaces.filter((p) => p.gate).map((p) => ({ name: p.gate, element: p.gateElement })), monthElement),
545
+ star: groupByState(palaces.map((p) => ({ name: p.star, element: p.starElement })), monthElement),
546
+ stem: groupByState(palaces.filter((p) => p.tianPan).map((p) => ({ name: p.tianPan, element: p.stemElement })), monthElement),
547
+ },
421
548
  palaces,
422
549
  centerPalace,
423
550
  rules: [