import { ELEMENT_LABEL } from './constants'; import { detectRelations } from './chart-relations'; import { createChart } from './chart'; import { getElementRelation, isSupportingRelation } from './relations'; import { detectRoots } from './roots'; import { getSeasonalStates, getTwelveGrowthStage } from './season-context'; import type { BaziChart, Confidence, EvidenceSide, FiveElement, FourPillarsInput, HiddenStemOrder, Pillar, PillarKey, Relation, RootPosition, SeasonalState, SpecialPatternReview, StrengthState, StrengthV2Analysis, StrengthV2Evidence, StrengthV2EvidenceCategory, StrengthV2Facts, StrengthV2SourceModel, TwelveGrowthStage, } from './types'; const STRENGTH_LABEL: Record = { 'extremely-weak': '极弱', weak: '偏弱', balanced: '相对中和', strong: '偏强', 'extremely-strong': '极强', }; const RELATION_LABEL: Record = { companion: '同类比劫', resource: '印星生扶', output: '食伤泄身', wealth: '财星耗身', officer: '官杀克身', }; const ROOT_ORDER_UNITS: Record = { main: 3, middle: 2, residual: 1, }; const ROOT_POSITION_BONUS: Record = { year: 0, month: 2, day: 1, hour: 0, }; const HIDDEN_STEM_UNITS: Record = { main: 2, middle: 1, residual: 1, }; const SEASONAL_STATE_RULE: Record< SeasonalState, { side: EvidenceSide | 'neutral'; units: number } > = { wang: { side: 'supporting', units: 4 }, xiang: { side: 'supporting', units: 2 }, xiu: { side: 'neutral', units: 0 }, qiu: { side: 'opposing', units: 2 }, si: { side: 'opposing', units: 4 }, }; const GROWTH_STAGE_RULE: Record< TwelveGrowthStage, { side: EvidenceSide | 'neutral'; units: number } > = { changsheng: { side: 'supporting', units: 1 }, muyu: { side: 'neutral', units: 0 }, guandai: { side: 'supporting', units: 1 }, linguan: { side: 'supporting', units: 2 }, diwang: { side: 'supporting', units: 2 }, shuai: { side: 'neutral', units: 0 }, bing: { side: 'opposing', units: 1 }, si: { side: 'opposing', units: 1 }, mu: { side: 'opposing', units: 1 }, jue: { side: 'opposing', units: 1 }, tai: { side: 'neutral', units: 0 }, yang: { side: 'neutral', units: 0 }, }; type EvidenceInput = Omit; function addEvidence(evidence: StrengthV2Evidence[], input: EvidenceInput): void { evidence.push({ id: `V2-E${String(evidence.length + 1).padStart(2, '0')}`, ...input, }); } function classifyStrength(delta: number): StrengthState { if (delta >= 8) return 'extremely-strong'; if (delta >= 3) return 'strong'; if (delta <= -8) return 'extremely-weak'; if (delta <= -3) return 'weak'; return 'balanced'; } function supportingAndOpposingElements(dayMaster: FiveElement): { supporting: FiveElement[]; opposing: FiveElement[]; } { const elements: FiveElement[] = ['wood', 'fire', 'earth', 'metal', 'water']; return { supporting: elements.filter((element) => isSupportingRelation(getElementRelation(dayMaster, element)) ), opposing: elements.filter((element) => !isSupportingRelation(getElementRelation(dayMaster, element)) ), }; } function collectSeasonEvidence( chart: BaziChart, facts: StrengthV2Facts, evidence: StrengthV2Evidence[], ): void { const seasonalStates = facts.seasonContext?.seasonalStates ?? getSeasonalStates(chart.pillars.month.branch.name); const dayMasterState = seasonalStates.byElement[chart.dayMaster.element]; const stateRule = SEASONAL_STATE_RULE[dayMasterState.state]; addEvidence(evidence, { ruleId: 'v2.season.day-master-state', category: 'seasonal-state', sourceModels: ['season-context-v1'], sources: ['seasonContext.seasonalStates'], side: stateRule.side, applied: stateRule.units > 0, decisionUnits: stateRule.units, description: `月支${seasonalStates.monthBranch}属${seasonalStates.seasonLabel},日主${ELEMENT_LABEL[chart.dayMaster.element]}处于${dayMasterState.stateLabel},按 v2 季节表${stateRule.side === 'neutral' ? '不增减' : `计入${stateRule.units}单位`}。`, }); const primaryPillars: PillarKey[] = ['month', 'day']; for (const pillarKey of primaryPillars) { const pillar = chart.pillars[pillarKey]; const growth = facts.seasonContext?.twelveGrowth.byPillar[pillarKey] ?? { pillar: pillarKey, branch: pillar.branch.name, ...getTwelveGrowthStage(chart.dayMaster.stem, pillar.branch.name), }; const rule = GROWTH_STAGE_RULE[growth.stage]; addEvidence(evidence, { ruleId: `v2.season.twelve-growth.${pillarKey}`, category: 'twelve-growth', sourceModels: ['season-context-v1'], sources: [`seasonContext.twelveGrowth.byPillar.${pillarKey}`], side: rule.side, applied: rule.units > 0, decisionUnits: rule.units, description: `日主${chart.dayMaster.stem}在${pillarKey === 'month' ? '月令' : '日坐'}${growth.branch}为${growth.stageLabel},${rule.side === 'neutral' ? 'v2 只记录不计分' : `计入${rule.units}单位`}。`, }); } if (facts.seasonContext) { const { previousMonthBoundary, nextMonthBoundary } = facts.seasonContext.solarTerms; addEvidence(evidence, { ruleId: 'v2.season.solar-term-distance', category: 'solar-term-distance', sourceModels: ['season-context-v1'], sources: [ 'seasonContext.solarTerms.previousMonthBoundary', 'seasonContext.solarTerms.nextMonthBoundary', ], side: 'neutral', applied: false, decisionUnits: 0, description: `距前一节${previousMonthBoundary.name}${previousMonthBoundary.distanceSeconds}秒,距后一节${nextMonthBoundary.name}${nextMonthBoundary.distanceSeconds}秒;v2 不把时间距离线性换算为旺衰分数。`, }); } } function collectVisibleStemEvidence( chart: BaziChart, evidence: StrengthV2Evidence[], ): void { const pillars: Pillar[] = [ chart.pillars.year, chart.pillars.month, chart.pillars.hour, ]; for (const pillar of pillars) { const relation = getElementRelation(chart.dayMaster.element, pillar.stem.element); const supporting = isSupportingRelation(relation); addEvidence(evidence, { ruleId: 'v2.chart.visible-stem', category: 'visible-stem', sourceModels: ['chart'], sources: [`${pillar.key}.stem`], side: supporting ? 'supporting' : 'opposing', applied: true, decisionUnits: 2, description: `${pillar.key}干${pillar.stem.name}${ELEMENT_LABEL[pillar.stem.element]}明透,为${pillar.stem.tenGod},属于${RELATION_LABEL[relation]}。`, }); } } function rootKey(root: RootPosition): string { return `${root.pillar}:${root.source}`; } function collectHiddenStemAndRootEvidence( chart: BaziChart, roots: RootPosition[], evidence: StrengthV2Evidence[], ): void { const rootKeys = new Set(roots.map(rootKey)); const pillars: Pillar[] = [ chart.pillars.year, chart.pillars.month, chart.pillars.day, chart.pillars.hour, ]; for (const pillar of pillars) { pillar.branch.hiddenStems.forEach((hiddenStem, index) => { const source = `${pillar.key}.branch.hiddenStems[${index}]`; if (rootKeys.has(`${pillar.key}:${source}`)) return; const relation = getElementRelation(chart.dayMaster.element, hiddenStem.element); const supporting = isSupportingRelation(relation); addEvidence(evidence, { ruleId: 'v2.chart.hidden-stem', category: 'hidden-stem', sourceModels: ['chart'], sources: [source], side: supporting ? 'supporting' : 'opposing', applied: true, decisionUnits: HIDDEN_STEM_UNITS[hiddenStem.order], description: `${pillar.key}支${pillar.branch.name}藏${hiddenStem.name}${ELEMENT_LABEL[hiddenStem.element]}(${hiddenStem.order}),为${hiddenStem.tenGod},属于${RELATION_LABEL[relation]}。`, }); }); } for (const [rootIndex, root] of roots.entries()) { const orderUnits = ROOT_ORDER_UNITS[root.order]; const positionBonus = ROOT_POSITION_BONUS[root.pillar]; addEvidence(evidence, { ruleId: 'v2.roots.position-and-order', category: 'root', sourceModels: ['roots-v1'], sources: [`roots.roots[${rootIndex}]`, root.source], side: 'supporting', applied: true, decisionUnits: orderUnits + positionBonus, description: `${root.pillar}支${root.branch}藏${root.hiddenStem}为日主同五行根,属于${root.order};本中余气计${orderUnits}单位,${root.pillar === 'month' ? '月令位置加2单位' : root.pillar === 'day' ? '日坐位置加1单位' : '该柱位不加成'}。`, }); } } function collectRelationEvidence( chart: BaziChart, facts: StrengthV2Facts, roots: RootPosition[], evidence: StrengthV2Evidence[], ): number { const relations = facts.relations ?? detectRelations(chart); let unappliedCount = 0; for (const [relationIndex, relation] of relations.stems.entries()) { const sources = [`relations.stems[${relationIndex}]`]; if (relation.type === 'stem-control' && relation.members.some((member) => member.pillar === 'day')) { addEvidence(evidence, { ruleId: 'v2.relations.day-master-control', category: 'stem-control', sourceModels: ['relations-v1'], sources, side: 'opposing', applied: true, decisionUnits: 1, description: `${relation.description}关系直接涉及日主,额外计1单位泄耗克。`, }); } else { unappliedCount++; addEvidence(evidence, { ruleId: 'v2.relations.observation-only', category: 'relation-observation', sourceModels: ['relations-v1'], sources, side: 'neutral', applied: false, decisionUnits: 0, description: `${relation.description}v2 缺少合化或间接作用的成立条件,只记录不计分。`, }); } } for (const [relationIndex, relation] of relations.branches.entries()) { const sources = [`relations.branches[${relationIndex}]`]; if (relation.type === 'branch-clash') { const affectedRoots = roots .map((root, rootIndex) => ({ root, rootIndex })) .filter(({ root }) => relation.members.some((member) => member.pillar === root.pillar && member.value === root.branch )); if (affectedRoots.length > 0) { for (const { root, rootIndex } of affectedRoots) { addEvidence(evidence, { ruleId: 'v2.relations.root-clash', category: 'root-clash', sourceModels: ['relations-v1', 'roots-v1'], sources: [...sources, `roots.roots[${rootIndex}]`, root.source], side: 'opposing', applied: true, decisionUnits: 1, description: `${relation.description}涉及${root.pillar}支${root.branch}的${root.order}根;v2 只计1单位减力,不删除该根。`, }); } continue; } } unappliedCount++; addEvidence(evidence, { ruleId: 'v2.relations.observation-only', category: 'relation-observation', sourceModels: ['relations-v1'], sources, side: 'neutral', applied: false, decisionUnits: 0, description: `${relation.description}v2 不在缺少合化、受损程度等条件时静默增减旺衰,只记录不计分。`, }); } return unappliedCount; } function detectSpecialPattern( hasRoot: boolean, supporting: number, opposing: number, ): SpecialPatternReview { if (!hasRoot && supporting <= 4 && opposing >= 10) { return { status: 'review-required', candidate: 'following-weak', reason: '日主无根,v2 生扶不超过4且泄耗克不少于10;可能涉及从弱或假从,仍需人工复核。', }; } if (supporting >= 14 && opposing <= 3) { return { status: 'review-required', candidate: 'following-strong', reason: 'v2 生扶不少于14且泄耗克不超过3;可能涉及从强、专旺或假从,仍需人工复核。', }; } return { status: 'not-indicated', candidate: null, reason: 'v2 常规旺衰证据未触发从强或从弱候选条件。', }; } function confidenceFrom( delta: number, specialPattern: SpecialPatternReview, unappliedRelations: number, ): Confidence { if (specialPattern.status === 'review-required') return 'low'; if (Math.abs(delta) >= 8 && unappliedRelations === 0) return 'high'; if (Math.abs(delta) >= 3) return 'medium'; return unappliedRelations > 0 ? 'low' : 'medium'; } export function analyzeChartV2( chart: BaziChart, facts: StrengthV2Facts = {}, ): StrengthV2Analysis { const roots = facts.roots ?? detectRoots(chart); const evidence: StrengthV2Evidence[] = []; collectSeasonEvidence(chart, facts, evidence); collectVisibleStemEvidence(chart, evidence); collectHiddenStemAndRootEvidence(chart, roots.roots, evidence); const unappliedRelations = collectRelationEvidence( chart, facts, roots.roots, evidence, ); const supporting = evidence .filter((item) => item.applied && item.side === 'supporting') .reduce((sum, item) => sum + item.decisionUnits, 0); const opposing = evidence .filter((item) => item.applied && item.side === 'opposing') .reduce((sum, item) => sum + item.decisionUnits, 0); const delta = supporting - opposing; const strength = classifyStrength(delta); const specialPattern = detectSpecialPattern(roots.hasRoot, supporting, opposing); const elements = supportingAndOpposingElements(chart.dayMaster.element); const warnings = [ 'ziping-strength-v2 是公开、确定性的工程模型;决策量不是五行百分比或命理准确率。', '节气距离不线性计分;季末土旺分段仍未建模。', ]; if (unappliedRelations > 0) { warnings.push(`有${unappliedRelations}条关系事实因缺少合化或作用强度条件而只记录、不计分。`); } return { model: 'ziping-strength-v2', status: specialPattern.status === 'review-required' ? 'needs-special-pattern-review' : 'completed', strength, strengthLabel: STRENGTH_LABEL[strength], confidence: confidenceFrom(delta, specialPattern, unappliedRelations), dayMaster: chart.dayMaster, supportingElements: elements.supporting, opposingElements: elements.opposing, evidence, decisionBalance: { supporting, opposing, delta, thresholds: { extremelyStrong: 8, strong: 3, weak: -3, extremelyWeak: -8, }, }, specialPattern, warnings, }; } export function analyzePillarsV2( pillars: FourPillarsInput, ): StrengthV2Analysis { return analyzeChartV2(createChart(pillars)); }