From b03d84d479d72c0f1f3af9d7dcf6c009706ac6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=92=B0?= Date: Mon, 3 Aug 2026 10:03:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=A7=E5=85=AD=E5=A3=AC=E8=AF=81?= =?UTF-8?q?=E6=8D=AE=E9=93=BE=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- calculator/src/evidence.ts | 853 +++++++++++++++++++++++++++++++++++ calculator/src/run-liuren.ts | 9 + 2 files changed, 862 insertions(+) create mode 100644 calculator/src/evidence.ts diff --git a/calculator/src/evidence.ts b/calculator/src/evidence.ts new file mode 100644 index 0000000..844b49a --- /dev/null +++ b/calculator/src/evidence.ts @@ -0,0 +1,853 @@ +/** + * 大六壬证据链架构 + * 参考梅花易数 evidence 模式,为排盘结果增加结构化证据链 + * + * 核心原则: + * - 每个 fact 必须有具体的 limitation(不能是泛泛免责) + * - 证据链是增强层,不破坏现有排盘流程 + * - 支持/约束/反证三位一体 + */ + +import { + Stem, Branch, BRANCHES, branchIndex, + BRANCH_ELEMENT, BRANCH_CHONG, BRANCH_HAI, BRANCH_XING, + WUXING_SHENG, WUXING_KE, getXunKong, +} from './ganzhi.js'; +import { PlatePosition, buildPlate, getYueJiang } from './plate.js'; +import { TianJiang, assignGenerals, getNoblemanBranch, isDaytime } from './tianjiang.js'; +import { SiKeLesson, SiKeResult, STEM_JIGONG } from './sike.js'; +import { SanChuanResult } from './sanchuan.js'; + +// ═══════════════════════════════════════════ +// 类型定义 +// ═══════════════════════════════════════════ + +/** 证据阶段键 */ +export type LiurenStageKey = 'initial' | 'middle' | 'final'; + +/** LiurenFact 基础接口 — 所有 fact 的公共字段 */ +export interface LiurenFact { + key: string; + status: string; + promptText: string; + sources: string[]; + limitation: string; +} + +/** 起盘参数事实 */ +export interface CalculationFact extends LiurenFact { + dayStem: Stem; + dayBranch: Branch; + hourBranch: Branch; + month: number; + yueJiang: Branch; + isDaytime: boolean; + noblemanBranch: Branch; + xunKong: Branch[]; +} + +/** 天地盘逐位事实 */ +export interface PlateFact extends LiurenFact { + position: number; // 0-11 + earthBranch: Branch; + heavenBranch: Branch; + general: TianJiang | undefined; + generalNature: string; // 天将吉凶 +} + +/** 四课事实 */ +export interface LessonEvidence extends LiurenFact { + lessonIndex: number; // 1-4 + lower: Branch; // 下神 + upper: Branch; // 上神 + lowerElement: string; + upperElement: string; + general: TianJiang | undefined; + relation: string; // 上下神五行关系 + lessonNote: string; // 课注 +} + +/** 三传事实 */ +export interface TransmissionEvidence extends LiurenFact { + stage: LiurenStageKey; + stageLabel: string; // 初传/中传/末传 + branch: Branch; + element: string; + general: TianJiang | undefined; + generalNature: string; + xunkongStatus: string; // 旬空状态 + seasonState: string; // 月令旺衰 +} + +/** 相邻传推进事实 */ +export interface TransitionFact extends LiurenFact { + fromStage: LiurenStageKey; + toStage: LiurenStageKey; + fromBranch: Branch; + toBranch: Branch; + fromElement: string; + toElement: string; + elementRelation: string; // 五行关系 + fromGeneral: string; + toGeneral: string; +} + +/** 反证事实 */ +export interface CounterEvidenceFact extends LiurenFact { + ownerStageKey: string; + type: '旬空' | '休囚' | '冲克' | '白虎临课' | '天空临课' | '玄武临课' | '脱耗' | '受克'; + detail: string; +} + +/** 应期事实 */ +export interface TimingFact extends LiurenFact { + order: number; + type: '初传旺衰' | '旬空填实' | '冲合时机' | '月令助力' | '传进条件'; + rawText?: string; +} + +/** 类神焦点事实 */ +export interface FocusFact extends LiurenFact { + category: string; // 问事类型 + targetBranch: Branch; + targetElement: string; + targetGeneral: string | undefined; + focusNote: string; +} + +/** 完整的证据链分析结果 */ +export interface LiurenEvidenceAnalysis { + key: 'liuren:evidence'; + status: '已计算'; + // 起盘参数 + calculation: CalculationFact; + // 天地盘 + plateFacts: PlateFact[]; + // 四课 + lessonFacts: LessonEvidence[]; + // 三传 + transmissionFacts: TransmissionEvidence[]; + // 相邻传推进 + transitionFacts: TransitionFact[]; + // 反证 + counterEvidenceFacts: CounterEvidenceFact[]; + counterSummary: { status: '有明确反证' | '未见明确反证'; detail: string }; + // 应期 + timingFacts: TimingFact[]; + // 类神焦点 + focusFacts: FocusFact[]; + // 汇总 + summary: { + factCount: number; + plateCount: number; + lessonCount: number; + transmissionCount: number; + transitionCount: number; + counterCount: number; + timingCount: number; + focusCount: number; + }; + // 全局限制 + limitations: string[]; + // 原始 prompt 文本 + promptText: string; +} + +// ═══════════════════════════════════════════ +// 限制声明常量 +// ═══════════════════════════════════════════ + +const CALCULATION_FACT_LIMITATION = + '起盘参数事实只记录四柱、月将、昼夜贵人、旬空等排盘基础条件;不得由单个参数直接推断现实事件的吉凶、成败或必然走向' as const; + +const PLATE_FACT_LIMITATION = + '天地盘逐位事实只记录地盘支、天盘支与所乘天将的排布关系;不得由单个天将位置直接断定现实人物意图、事件结果或时间确定性' as const; + +const LESSON_FACT_LIMITATION = + '四课事实只记录上下神五行关系与所乘天将的状态;课注是传统分类辅助,不得将单课关系直接等同于现实事件的起因、过程或结果' as const; + +const TRANSMISSION_FACT_LIMITATION = + '三传事实只记录初传、中传、末传的地支、天将、旬空与月令状态;三传推进是盘内逻辑推演,不得直接等同于现实事件的必然时间线或固定结果' as const; + +const TRANSITION_FACT_LIMITATION = + '相邻传推进事实只比较初传→中传、中传→末传的五行关系变化;不得把传内先后直接写成现实事件必然按同样顺序发生' as const; + +const COUNTER_FACT_LIMITATION = + '反证事实只表示盘面存在旬空、休囚、冲克、白虎等限制条件;不得把单项反证直接写成现实失败、灾祸、伤病或必然结果' as const; + +const COUNTER_SUMMARY_LIMITATION = + '反证汇总只说明当前盘面核验是否发现明确限制;未见明确反证不代表现实风险为零,也不得按反证数量换算吉凶总分' as const; + +const TIMING_FACT_LIMITATION = + '应期事实只提供旺衰、旬空填实、冲合等相对触发条件;不得把地支、五行、旺衰换算为唯一日期,也不证明事件必然发生' as const; + +const FOCUS_FACT_LIMITATION = + '类神焦点事实只根据问事类型在盘面中定位关注对象的状态;类神选取基于传统分类规则,不代表该对象在现实中必然表现为所述状态' as const; + +const SUMMARY_FACT_LIMITATION = + '大六壬证据汇总只统计起盘参数、天地盘、四课、三传、推进、反证、应期与类神事实的覆盖情况;不得按数量生成吉凶总分、成功率或唯一日期' as const; + +// ═══════════════════════════════════════════ +// 辅助函数 +// ═══════════════════════════════════════════ + +function unique(values: string[]): string[] { + return Array.from(new Set(values.filter(Boolean))); +} + +/** 判断旺相休囚死 */ +function getSeasonState(element: string, monthBranch: Branch): string { + const monthElement = BRANCH_ELEMENT[monthBranch]; + if (element === monthElement) return '旺'; + if (WUXING_SHENG[monthElement] === element) return '相'; + if (WUXING_SHENG[element] === monthElement) return '休'; + if (WUXING_KE[element] === monthElement) return '囚'; + return '死'; +} + +/** 获取两支五行关系描述 */ +function getElementRelation(from: Branch, to: Branch): string { + const fe = BRANCH_ELEMENT[from]; + const te = BRANCH_ELEMENT[to]; + if (fe === te) return '比和'; + if (WUXING_SHENG[fe] === te) return `${fe}生${te}`; + if (WUXING_KE[fe] === te) return `${fe}克${te}`; + if (WUXING_SHENG[te] === fe) return `${te}生${fe}`; + if (WUXING_KE[te] === fe) return `${te}克${fe}`; + return '关系未定'; +} + +/** 检查地支之间的冲关系 */ +function hasChong(a: Branch, b: Branch): boolean { + return BRANCH_CHONG[a] === b; +} + +/** 检查地支之间的害关系 */ +function hasHai(a: Branch, b: Branch): boolean { + return BRANCH_HAI[a] === b; +} + +/** 检查地支之间的刑关系 */ +function hasXing(a: Branch, b: Branch): boolean { + return (BRANCH_XING[a] || []).includes(b); +} + +/** 天将简述 */ +function generalBrief(g: TianJiang | undefined): string { + return g ? `${g.name}(${g.element}/${g.nature})` : '无将'; +} + +// ═══════════════════════════════════════════ +// 证据链生成函数 +// ═══════════════════════════════════════════ + +export interface LiurenInput { + dayStem: Stem; + dayBranch: Branch; + hourBranch: Branch; + month: number; + yueJiang: Branch; + plate: PlatePosition[]; + heavenBranches: Branch[]; + sike: SiKeResult; + sanchuan: SanChuanResult; + generals: Map; + noblemanBranch: Branch; + xunKong: Branch[]; + question?: string; +} + +/** + * 从排盘结果生成完整证据链 + */ +export function buildEvidence(input: LiurenInput): LiurenEvidenceAnalysis { + const { + dayStem, dayBranch, hourBranch, month, yueJiang, + plate, heavenBranches, sike, sanchuan, + generals, noblemanBranch, xunKong, question, + } = input; + + const monthBranch = BRANCHES[(month + 1) % 12] as Branch; // 简化月支映射 + + // ── 1. 起盘参数 ── + const calculation = buildCalculationFact( + dayStem, dayBranch, hourBranch, month, yueJiang, + noblemanBranch, xunKong + ); + + // ── 2. 天地盘逐位 ── + const plateFacts = buildPlateFacts(plate, generals); + + // ── 3. 四课 ── + const lessonFacts = buildLessonFacts(sike, generals); + + // ── 4. 三传 ── + const transmissionFacts = buildTransmissionFacts(sanchuan, generals, xunKong, monthBranch); + + // ── 5. 相邻传推进 ── + const transitionFacts = buildTransitionFacts(transmissionFacts); + + // ── 6. 反证 ── + const counterEvidenceFacts = buildCounterEvidence( + sike, sanchuan, generals, xunKong, monthBranch + ); + const counterSummary = { + status: counterEvidenceFacts.length > 0 ? '有明确反证' as const : '未见明确反证' as const, + detail: counterEvidenceFacts.length > 0 + ? `发现${counterEvidenceFacts.length}条反证:${counterEvidenceFacts.map(f => f.detail).join(';')}` + : '当前盘面未见明确反证', + limitation: COUNTER_SUMMARY_LIMITATION, + }; + + // ── 7. 应期 ── + const timingFacts = buildTimingFacts(sanchuan, xunKong, monthBranch); + + // ── 8. 类神焦点 ── + const focusFacts = buildFocusFacts(question, sike, sanchuan, plate, generals, monthBranch); + + // ── 汇总 ── + const factCount = 1 + plateFacts.length + lessonFacts.length + + transmissionFacts.length + transitionFacts.length + + counterEvidenceFacts.length + timingFacts.length + focusFacts.length; + const summary = { + factCount, + plateCount: plateFacts.length, + lessonCount: lessonFacts.length, + transmissionCount: transmissionFacts.length, + transitionCount: transitionFacts.length, + counterCount: counterEvidenceFacts.length, + timingCount: timingFacts.length, + focusCount: focusFacts.length, + limitation: SUMMARY_FACT_LIMITATION, + }; + + // ── 全局限制 ── + const limitations = [ + '大六壬证据链仅描述天地盘、四课、三传的五行生克与天将状态,不证明现实吉凶、成败、概率或固定应期', + '四课课注与传统断语是辅助分类,不直接对应现实事件、人物、婚育、疾病、伤亡或诉讼', + '反证与支持仅反映盘面五行生克状态,不代表现实中的必然结果或风险为零', + '应期分析只提供相对触发条件(旺衰、填实、冲合),不得换算为绝对日期或时间节点', + '类神焦点基于传统问事分类规则选取,不代表该对象在现实中必然表现为所述状态', + ]; + + // ── promptText ── + const promptText = [ + calculation.promptText, + ...plateFacts.map(f => f.promptText), + ...lessonFacts.map(f => f.promptText), + ...transmissionFacts.map(f => f.promptText), + ...transitionFacts.map(f => f.promptText), + ...counterEvidenceFacts.map(f => f.promptText), + counterSummary.detail, + ...timingFacts.map(f => f.promptText), + ...focusFacts.map(f => f.promptText), + ].join('\n'); + + return { + key: 'liuren:evidence', + status: '已计算', + calculation, + plateFacts, + lessonFacts, + transmissionFacts, + transitionFacts, + counterEvidenceFacts, + counterSummary, + timingFacts, + focusFacts, + summary, + limitations, + promptText, + }; +} + +// ── 子生成器 ── + +function buildCalculationFact( + dayStem: Stem, dayBranch: Branch, hourBranch: Branch, + month: number, yueJiang: Branch, + noblemanBranch: Branch, xunKong: Branch[], +): CalculationFact { + const daytime = isDaytime(hourBranch); + return { + key: 'liuren:calc', + status: '已记录', + dayStem, dayBranch, hourBranch, month, yueJiang, + isDaytime: daytime, + noblemanBranch, + xunKong, + promptText: `起盘:${dayStem}${dayBranch}日 ${hourBranch}时,${month}月月将${yueJiang},${daytime ? '昼' : '夜'}占,贵人${noblemanBranch},旬空${xunKong.join('、')}`, + sources: ['四柱日时', '月将起法', '昼夜贵人规则', '旬空起法'], + limitation: CALCULATION_FACT_LIMITATION, + }; +} + +function buildPlateFacts( + plate: PlatePosition[], + generals: Map, +): PlateFact[] { + return BRANCHES.map((earth, i) => { + const heaven = plate[i].heavenBranch; + const gen = generals.get(earth); + return { + key: `liuren:plate:${earth}`, + status: '已记录', + position: i, + earthBranch: earth, + heavenBranch: heaven, + general: gen, + generalNature: gen?.nature || '', + promptText: `${earth}上见${heaven},乘${generalBrief(gen)}`, + sources: ['天地盘排布', '十二天将排布'], + limitation: PLATE_FACT_LIMITATION, + }; + }); +} + +function buildLessonFacts( + sike: SiKeResult, + generals: Map, +): LessonEvidence[] { + return sike.lessons.map(lesson => { + const lowerElement = BRANCH_ELEMENT[lesson.lower]; + const upperElement = BRANCH_ELEMENT[lesson.upper]; + const gen = generals.get(lesson.upper); + const relation = getElementRelation(lesson.lower, lesson.upper); + + // 课注 + const notes: string[] = []; + if (lowerElement === upperElement) notes.push('上下比和'); + if (WUXING_KE[upperElement] === lowerElement) notes.push('上克下(克)'); + if (WUXING_KE[lowerElement] === upperElement) notes.push('下克上(贼)'); + if (WUXING_SHENG[upperElement] === lowerElement) notes.push('上生下'); + if (WUXING_SHENG[lowerElement] === upperElement) notes.push('下生上(泄)'); + if (hasChong(lesson.lower, lesson.upper)) notes.push('上下相冲'); + if (hasHai(lesson.lower, lesson.upper)) notes.push('上下相害'); + if (hasXing(lesson.lower, lesson.upper)) notes.push('上下相刑'); + + const lessonNote = notes.length > 0 ? notes.join(';') : '无特殊格局'; + + return { + key: `liuren:lesson:${lesson.index}`, + status: '已计算', + lessonIndex: lesson.index, + lower: lesson.lower, + upper: lesson.upper, + lowerElement, + upperElement, + general: gen, + relation, + lessonNote, + promptText: `第${lesson.index}课:${lesson.lower}(${lowerElement})→${lesson.upper}(${upperElement}),乘${generalBrief(gen)},${relation};${lessonNote}`, + sources: ['四课上下神', '五行生克', '天将排布', '冲害刑关系'], + limitation: LESSON_FACT_LIMITATION, + }; + }); +} + +function buildTransmissionFacts( + sanchuan: SanChuanResult, + generals: Map, + xunKong: Branch[], + monthBranch: Branch, +): TransmissionEvidence[] { + const stages: { key: LiurenStageKey; label: string; branch: Branch }[] = [ + { key: 'initial', label: '初传(发用)', branch: sanchuan.initial }, + { key: 'middle', label: '中传(过程)', branch: sanchuan.middle }, + { key: 'final', label: '末传(结果)', branch: sanchuan.final }, + ]; + + return stages.map(({ key, label, branch }) => { + const element = BRANCH_ELEMENT[branch]; + const gen = generals.get(branch); + const xunkongStatus = xunKong.includes(branch) ? '旬空' : '不空'; + const seasonState = getSeasonState(element, monthBranch); + + return { + key: `liuren:transmission:${key}`, + status: '已计算', + stage: key, + stageLabel: label, + branch, + element, + general: gen, + generalNature: gen?.nature || '', + xunkongStatus, + seasonState, + promptText: `${label}:${branch}(${element}),乘${generalBrief(gen)},${xunkongStatus},月令${seasonState}`, + sources: ['三传取法', '五行旺衰', '旬空判断', '天将排布'], + limitation: TRANSMISSION_FACT_LIMITATION, + }; + }); +} + +function buildTransitionFacts(transmissions: TransmissionEvidence[]): TransitionFact[] { + const transitions: TransitionFact[] = []; + for (let i = 0; i < transmissions.length - 1; i++) { + const from = transmissions[i]; + const to = transmissions[i + 1]; + const elementRelation = getElementRelation(from.branch, to.branch); + const relationChanged = from.element !== to.element; + + transitions.push({ + key: `liuren:transition:${from.stage}-${to.stage}`, + status: '连续', + fromStage: from.stage, + toStage: to.stage, + fromBranch: from.branch, + toBranch: to.branch, + fromElement: from.element, + toElement: to.element, + elementRelation, + fromGeneral: generalBrief(from.general), + toGeneral: generalBrief(to.general), + promptText: `${from.stageLabel}→${to.stageLabel}:${from.branch}(${from.element})→${to.branch}(${to.element}),${elementRelation};将${generalBrief(from.general)}→${generalBrief(to.general)}`, + sources: ['三传五行关系', '天将变化'], + limitation: TRANSITION_FACT_LIMITATION, + }); + } + return transitions; +} + +function buildCounterEvidence( + sike: SiKeResult, + sanchuan: SanChuanResult, + generals: Map, + xunKong: Branch[], + monthBranch: Branch, +): CounterEvidenceFact[] { + const counters: CounterEvidenceFact[] = []; + + // 三传旬空 + const allTransmissions: { label: string; branch: Branch }[] = [ + { label: '初传', branch: sanchuan.initial }, + { label: '中传', branch: sanchuan.middle }, + { label: '末传', branch: sanchuan.final }, + ]; + for (const t of allTransmissions) { + if (xunKong.includes(t.branch)) { + counters.push({ + key: `liuren:counter:xunkong:${t.label}`, + ownerStageKey: t.label, + type: '旬空', + status: '已触发', + detail: `${t.label}${t.branch}旬空,事象虚而不实`, + promptText: `反证:${t.label}${t.branch}旬空,该阶段事象有落空之虞`, + sources: ['旬空起法'], + limitation: COUNTER_FACT_LIMITATION, + }); + } + } + + // 三传休囚 + const transmissionElements: { label: string; branch: Branch }[] = [ + { label: '初传', branch: sanchuan.initial }, + { label: '中传', branch: sanchuan.middle }, + { label: '末传', branch: sanchuan.final }, + ]; + for (const t of transmissionElements) { + const el = BRANCH_ELEMENT[t.branch]; + const state = getSeasonState(el, monthBranch); + if (state === '休' || state === '囚' || state === '死') { + counters.push({ + key: `liuren:counter:season:${t.label}`, + ownerStageKey: t.label, + type: '休囚', + status: '已触发', + detail: `${t.label}${t.branch}(${el})月令${state},不得时`, + promptText: `反证:${t.label}${t.branch}(${el})月令${state},该阶段不得时令助力`, + sources: ['月令旺衰'], + limitation: COUNTER_FACT_LIMITATION, + }); + } + } + + // 四课中白虎临课 + for (const lesson of sike.lessons) { + const gen = generals.get(lesson.upper); + if (gen && gen.name === '白虎') { + counters.push({ + key: `liuren:counter:baihu:lesson${lesson.index}`, + ownerStageKey: `第${lesson.index}课`, + type: '白虎临课', + status: '已触发', + detail: `第${lesson.index}课上神${lesson.upper}乘白虎`, + promptText: `反证:第${lesson.index}课${lesson.lower}→${lesson.upper}乘白虎,主凶险、伤灾、丧服之象`, + sources: ['天将排布', '白虎主事'], + limitation: COUNTER_FACT_LIMITATION, + }); + } + } + + // 四课中天空临课 + for (const lesson of sike.lessons) { + const gen = generals.get(lesson.upper); + if (gen && gen.name === '天空') { + counters.push({ + key: `liuren:counter:tiankong:lesson${lesson.index}`, + ownerStageKey: `第${lesson.index}课`, + type: '天空临课', + status: '已触发', + detail: `第${lesson.index}课上神${lesson.upper}乘天空`, + promptText: `反证:第${lesson.index}课${lesson.lower}→${lesson.upper}乘天空,主虚诈、不实、空亡之象`, + sources: ['天将排布', '天空主事'], + limitation: COUNTER_FACT_LIMITATION, + }); + } + } + + // 四课中玄武临课 + for (const lesson of sike.lessons) { + const gen = generals.get(lesson.upper); + if (gen && gen.name === '玄武') { + counters.push({ + key: `liuren:counter:xuanwu:lesson${lesson.index}`, + ownerStageKey: `第${lesson.index}课`, + type: '玄武临课', + status: '已触发', + detail: `第${lesson.index}课上神${lesson.upper}乘玄武`, + promptText: `反证:第${lesson.index}课${lesson.lower}→${lesson.upper}乘玄武,主暗昧、盗失、欺诈之象`, + sources: ['天将排布', '玄武主事'], + limitation: COUNTER_FACT_LIMITATION, + }); + } + } + + // 四课冲克 + for (const lesson of sike.lessons) { + if (hasChong(lesson.lower, lesson.upper)) { + counters.push({ + key: `liuren:counter:chong:lesson${lesson.index}`, + ownerStageKey: `第${lesson.index}课`, + type: '冲克', + status: '已触发', + detail: `第${lesson.index}课${lesson.lower}与${lesson.upper}相冲`, + promptText: `反证:第${lesson.index}课上下神${lesson.lower}↔${lesson.upper}相冲,主动荡、分离、不稳`, + sources: ['地支六冲'], + limitation: COUNTER_FACT_LIMITATION, + }); + } + } + + // 三传脱耗(初传生日干所寄之支,即泄气) + const ganJiGong = STEM_JIGONG[sike.dayStem]; + for (const t of allTransmissions) { + const relation = getElementRelation(t.branch, ganJiGong); + if (relation.includes('生') && WUXING_SHENG[BRANCH_ELEMENT[t.branch]] === BRANCH_ELEMENT[ganJiGong]) { + // 初传五行生日干所寄五行 = 脱耗 + } + } + + // 三传受克(被日干所寄之支克制,不算反证;反过来,三传克日干寄宫才算) + for (const t of allTransmissions) { + const tEl = BRANCH_ELEMENT[t.branch]; + const gEl = BRANCH_ELEMENT[ganJiGong]; + if (WUXING_KE[gEl] === tEl) { + // 日干克三传 → 三传受克 + counters.push({ + key: `liuren:counter:shouke:${t.label}`, + ownerStageKey: t.label, + type: '受克', + status: '已触发', + detail: `${t.label}${t.branch}(${tEl})受日干${sike.dayStem}(${gEl})克制`, + promptText: `反证:${t.label}${t.branch}(${tEl})受日干${sike.dayStem}(${gEl})克制,该阶段受制`, + sources: ['五行克关系'], + limitation: COUNTER_FACT_LIMITATION, + }); + } + } + + return counters; +} + +function buildTimingFacts( + sanchuan: SanChuanResult, + xunKong: Branch[], + monthBranch: Branch, +): TimingFact[] { + const facts: TimingFact[] = []; + const initialElement = BRANCH_ELEMENT[sanchuan.initial]; + const initialState = getSeasonState(initialElement, monthBranch); + + // 初传旺衰 + facts.push({ + key: 'liuren:timing:initialWang', + order: 1, + type: '初传旺衰', + status: '已计算', + rawText: `初传${sanchuan.initial}(${initialElement})月令${initialState}`, + promptText: `应期参考:初传${sanchuan.initial}(${initialElement})月令${initialState},${initialState === '旺' || initialState === '相' ? '初传得令,应期较近' : '初传失令,应期较远或需待旺时'}`, + sources: ['初传五行', '月令旺衰'], + limitation: TIMING_FACT_LIMITATION, + }); + + // 旬空填实 + if (xunKong.includes(sanchuan.initial)) { + facts.push({ + key: 'liuren:timing:xunkongFill', + order: 2, + type: '旬空填实', + status: '已计算', + rawText: `初传${sanchuan.initial}旬空,需待填实`, + promptText: `应期参考:初传${sanchuan.initial}旬空,事之初虚而不实,需待该支当令或日辰填实方应`, + sources: ['旬空起法', '填实规则'], + limitation: TIMING_FACT_LIMITATION, + }); + } + + // 冲合时机 + const finalBranch = sanchuan.final; + const finalChong = BRANCH_CHONG[finalBranch]; + facts.push({ + key: 'liuren:timing:chongHe', + order: 3, + type: '冲合时机', + status: '已计算', + rawText: `末传${finalBranch},冲${finalChong}`, + promptText: `应期参考:末传${finalBranch},逢${finalChong}冲则动,逢合则应;冲主动,合主成`, + sources: ['地支六合', '地支六冲'], + limitation: TIMING_FACT_LIMITATION, + }); + + // 月令助力 + facts.push({ + key: 'liuren:timing:monthHelp', + order: 4, + type: '月令助力', + status: '已计算', + rawText: `月支${BRANCHES[(branchIndex(monthBranch) + 1) % 12]}(简化)`, + promptText: `应期参考:当月月支对初传${sanchuan.initial}(${initialElement})的影响,得令则助力,失令则受制`, + sources: ['月令五行'], + limitation: TIMING_FACT_LIMITATION, + }); + + return facts; +} + +function buildFocusFacts( + question: string | undefined, + sike: SiKeResult, + sanchuan: SanChuanResult, + plate: PlatePosition[], + generals: Map, + monthBranch: Branch, +): FocusFact[] { + if (!question) return []; + + const facts: FocusFact[] = []; + const q = question.toLowerCase(); + + // 问事分类 → 类神映射 + const categoryMap: { pattern: RegExp; category: string; targetBranch: Branch; note: string }[] = [ + { pattern: /财|钱|利|生意|投资|交易/, category: '求财', targetBranch: '辰', note: '财以日干所克为类神,辰为水库财源' }, + { pattern: /官|升|职|仕|考|面试|领导/, category: '求官', targetBranch: '酉', note: '官以克日干者为类神' }, + { pattern: /病|疾|医|健|身体/, category: '疾病', targetBranch: '巳', note: '病以日干及天医为类神' }, + { pattern: /婚|恋|爱|对象|配偶|嫁娶/, category: '婚姻', targetBranch: '午', note: '婚以干支上神合为类神' }, + { pattern: /行|出行|旅|出差|路上/, category: '出行', targetBranch: '卯', note: '出行以天罡、驿马为类神' }, + { pattern: /失|丢|找|遗|寻/, category: '寻物', targetBranch: '申', note: '寻物以玄武所乘为类神' }, + { pattern: /讼|法|官司|诉讼|纠纷/, category: '诉讼', targetBranch: '寅', note: '讼以朱雀为类神' }, + ]; + + for (const { pattern, category, targetBranch, note } of categoryMap) { + if (pattern.test(q)) { + const gen = generals.get(targetBranch); + const element = BRANCH_ELEMENT[targetBranch]; + const state = getSeasonState(element, monthBranch); + + facts.push({ + key: `liuren:focus:${category}`, + status: '已定位', + category, + targetBranch, + targetElement: element, + targetGeneral: gen?.name, + focusNote: note, + promptText: `类神焦点【${category}】:${targetBranch}(${element}),乘${generalBrief(gen)},月令${state};${note}`, + sources: ['问事分类', '类神传统规则', '天将排布'], + limitation: FOCUS_FACT_LIMITATION, + }); + + break; // 只取第一个匹配 + } + } + + return facts; +} + +// ═══════════════════════════════════════════ +// 格式化输出 +// ═══════════════════════════════════════════ + +/** 将证据链格式化为可读文本段落 */ +export function formatEvidenceText(ev: LiurenEvidenceAnalysis): string { + let text = `\n═══ 证 据 链 ═══\n\n`; + + // 起盘参数 + text += `【起盘参数】${ev.calculation.promptText}\n\n`; + + // 天地盘(压缩为一行摘要) + text += `【天地盘】`; + for (const f of ev.plateFacts) { + text += `${f.earthBranch}→${f.heavenBranch}(${f.general?.name || '-'}) `; + } + text += `\n\n`; + + // 四课 + text += `【四课证据】\n`; + for (const f of ev.lessonFacts) { + text += ` ${f.promptText}\n`; + } + text += `\n`; + + // 三传 + text += `【三传证据】\n`; + for (const f of ev.transmissionFacts) { + text += ` ${f.promptText}\n`; + } + text += `\n`; + + // 推进 + if (ev.transitionFacts.length > 0) { + text += `【传间推进】\n`; + for (const f of ev.transitionFacts) { + text += ` ${f.promptText}\n`; + } + text += `\n`; + } + + // 反证 + text += `【反证】${ev.counterSummary.detail}\n`; + for (const f of ev.counterEvidenceFacts) { + text += ` ⚠️ ${f.promptText}\n`; + } + text += `\n`; + + // 应期 + text += `【应期参考】\n`; + for (const f of ev.timingFacts) { + text += ` ${f.promptText}\n`; + } + text += `\n`; + + // 类神焦点 + if (ev.focusFacts.length > 0) { + text += `【类神焦点】\n`; + for (const f of ev.focusFacts) { + text += ` ${f.promptText}\n`; + } + text += `\n`; + } + + // 汇总 + text += `【证据链汇总】共${ev.summary.factCount}条事实:`; + text += `天地盘${ev.summary.plateCount}条,`; + text += `四课${ev.summary.lessonCount}条,`; + text += `三传${ev.summary.transmissionCount}条,`; + text += `推进${ev.summary.transitionCount}条,`; + text += `反证${ev.summary.counterCount}条,`; + text += `应期${ev.summary.timingCount}条`; + if (ev.summary.focusCount > 0) text += `,类神${ev.summary.focusCount}条`; + text += `\n`; + + return text; +} diff --git a/calculator/src/run-liuren.ts b/calculator/src/run-liuren.ts index c8eb195..e027336 100644 --- a/calculator/src/run-liuren.ts +++ b/calculator/src/run-liuren.ts @@ -3,6 +3,7 @@ import { buildPlate, getYueJiang, formatPlate } from './plate.js'; import { assignGenerals, formatGenerals, getNoblemanBranch, isDaytime } from './tianjiang.js'; import { buildSiKe, STEM_JIGONG } from './sike.js'; import { buildSanChuan } from './sanchuan.js'; +import { buildEvidence, formatEvidenceText } from './evidence.js'; // CLI 参数解析 const args = process.argv.slice(2); @@ -57,6 +58,14 @@ try { }, null, 2)); } else { console.log(formatAsText(dayStem, dayBranch, hourBranch, month, yueJiang, plate, heavenBranches, sike, sanchuan, generals, noblemanBranch, xunKong, question)); + + // 证据链 + const evidence = buildEvidence({ + dayStem, dayBranch, hourBranch, month, yueJiang, + plate, heavenBranches, sike, sanchuan, + generals, noblemanBranch, xunKong, question, + }); + console.log(formatEvidenceText(evidence)); } } catch (e: any) { console.error('排盘错误:', e.message);