/** * 生时校正引擎 v2.0 — 专业印度占星方法 * 基于 K.N. Rao + P.V.R. Narasimha Rao 方法论 * 多维评分:Dasha(40%) + 分盘一致性(35%) + 宫位变化(15%) + Nakshatra Pada(10%) */ import { computeChart, computeDasha, SIGNS, SIGNS_CN, SIGN_LORDS, PLANET_CN, NAKSHATRA_LIST } from './jyotish-engine.js'; import { computeVarga } from './jyotish-advanced.js'; export const VARGA_SENSITIVITY = { D1:{min:120,cn:'本命盘',en:'Rasi'},D4:{min:7.5,cn:'房产盘',en:'Chaturthamsa'}, D7:{min:4.3,cn:'子女盘',en:'Saptamsa'},D9:{min:13.3,cn:'九分盘',en:'Navamsa'}, D10:{min:12,cn:'事业盘',en:'Dasamsa'},D12:{min:10,cn:'父母盘',en:'Dwadasamsa'}, D24:{min:5,cn:'教育盘',en:'Siddhamsa'},D30:{min:4,cn:'厄运盘',en:'Trimsamsa'}, D60:{min:2,cn:'业力盘',en:'Shashtiamsa'}, }; export const EVENT_CATEGORIES = { marriage: {cn:'婚姻/恋爱', en:'Marriage/Romance', icon:'💍',planets:['Venus','Jupiter'],houses:[7], varga:'D9'}, divorce: {cn:'离婚/分离', en:'Divorce/Separation',icon:'💔',planets:['Rahu','Saturn'], houses:[7,8],varga:'D9'}, relationship:{cn:'重要关系', en:'Key Relationship', icon:'🤝',planets:['Venus','Moon'], houses:[7,1],varga:'D9'}, career: {cn:'事业/升职', en:'Career/Promotion', icon:'💼',planets:['Sun','Saturn'], houses:[10], varga:'D10'}, job_change: {cn:'工作变动', en:'Job Change', icon:'🔄',planets:['Saturn','Sun'], houses:[10,6],varga:'D10'}, business: {cn:'创业/商业', en:'Business Venture', icon:'🏢',planets:['Mercury','Mars'], houses:[10,7],varga:'D10'}, education: {cn:'教育/考试', en:'Education/Exam', icon:'📚',planets:['Mercury','Jupiter'],houses:[4,5],varga:'D24'}, health: {cn:'健康/手术', en:'Health/Surgery', icon:'🏥',planets:['Mars','Rahu'], houses:[6], varga:'D30'}, child: {cn:'生育/子女', en:'Childbirth', icon:'👶',planets:['Jupiter','Sun'], houses:[5], varga:'D7'}, travel: {cn:'远行/搬迁', en:'Travel/Relocation',icon:'✈️',planets:['Rahu','Moon'], houses:[9,12,3],varga:'D1'}, finance_pos: {cn:'财运/收入', en:'Income/Gains', icon:'💰',planets:['Jupiter','Venus'],houses:[2,11],varga:'D2'}, finance_neg: {cn:'破财/损失', en:'Financial Loss', icon:'📉',planets:['Saturn','Rahu'], houses:[8,12],varga:'D2'}, property: {cn:'房产/置业', en:'Property', icon:'🏠',planets:['Mars','Venus'], houses:[4], varga:'D4'}, loss: {cn:'失去/离别', en:'Loss/Separation', icon:'🕊️',planets:['Saturn','Rahu'], houses:[8,12],varga:'D1'}, spiritual: {cn:'灵性/修行', en:'Spiritual Practice',icon:'🙏',planets:['Jupiter','Ketu'],houses:[9,12],varga:'D1'}, legal: {cn:'法律/纠纷', en:'Legal/Dispute', icon:'⚖️',planets:['Mars','Saturn'], houses:[6,8], varga:'D1'}, fame: {cn:'名声/荣誉', en:'Fame/Honor', icon:'🌟',planets:['Sun','Jupiter'], houses:[1,10,9],varga:'D10'}, }; export const EVENT_COLLECTION_GUIDE = [ { key: 'relationship', cn: '感情/婚姻/分离', en: 'relationship, marriage, separation', categories: ['marriage', 'divorce', 'relationship'] }, { key: 'career', cn: '入职/升职/创业/换工作', en: 'job start, promotion, business, job change', categories: ['career', 'job_change', 'business', 'fame'] }, { key: 'education', cn: '升学/考试/毕业', en: 'education, exam, graduation', categories: ['education'] }, { key: 'family', cn: '生育/父母/家庭房产', en: 'childbirth, parents, home, property', categories: ['child', 'property'] }, { key: 'stress', cn: '健康/事故/诉讼/破财', en: 'health, accident, legal dispute, loss', categories: ['health', 'legal', 'finance_neg', 'loss'] }, { key: 'mobility', cn: '搬家/远行/移民', en: 'relocation, travel, migration', categories: ['travel'] }, ]; export const RECTIFICATION_RECOMMENDED_EVENT_QUESTION_MAP = { career_change: { id: 'recommended_career_change', category: 'job_change', varga: 'D10', label_cn: '事业转折', label_en: 'career change', question_cn: '你是否有日期比较明确的换工作、项目解散、升职失败或职业转折?', question_en: 'Have you had a dated job change, project ending, or career pivot?', examples_cn: ['换工作', '项目解散', '升职/落选', '职业转向'], }, education_end: { id: 'recommended_education_end', category: 'education', varga: 'D24', label_cn: '学业完成', label_en: 'education completion', question_cn: '你是否有日期比较明确的毕业、升学、艺考培训结束或重要考试结果?', question_en: 'Have you had a dated graduation, admission, or important exam result?', examples_cn: ['毕业', '升学', '艺考培训', '考试结果'], }, relocation: { id: 'recommended_relocation', category: 'travel', varga: 'D4', label_cn: '迁移搬家', label_en: 'relocation', question_cn: '你是否有日期比较明确的搬家、长期异地、远行或迁移事件?', question_en: 'Have you had a dated move, long-distance stay, or relocation?', examples_cn: ['搬家', '异地求学', '外地工作', '迁移'], }, marriage: { id: 'recommended_marriage', category: 'marriage', varga: 'D9', label_cn: '婚恋关系', label_en: 'marriage relationship', question_cn: '你是否有日期比较明确的恋爱开始、订婚、结婚或关系破裂?', question_en: 'Have you had a dated romance, engagement, marriage, or breakup?', examples_cn: ['恋爱开始', '订婚', '结婚', '分手'], }, windfall: { id: 'recommended_windfall', category: 'finance_pos', varga: 'D2', label_cn: '收入起落', label_en: 'income change', question_cn: '你是否有日期比较明确的奖金、定金、收入明显增加或突然破财?', question_en: 'Have you had a dated bonus, deposit, income jump, or sudden loss?', examples_cn: ['奖金', '定金', '收入增长', '破财'], }, }; const RECTIFICATION_THEME_VARGAS = { marriage: ['D9'], divorce: ['D9'], relationship: ['D9'], career: ['D10'], job_change: ['D10'], business: ['D10'], fame: ['D10'], child: ['D7'], parents: ['D12'], education: ['D24'], property: ['D4'], finance_pos: ['D2'], finance_neg: ['D2'], health: ['D30'], legal: ['D30'], loss: ['D30'], travel: ['D4'], }; // ——— 工具函数 ——— export function getHouseLord(ai, h) { return SIGN_LORDS[SIGNS[(ai + h - 1) % 12]]; } export function offsetBirth(b, offMin) { const t = b.hour * 60 + b.minute + offMin; const d = new Date(b.year, b.month - 1, b.day, 0, 0, 0); d.setMinutes(d.getMinutes() + t); return {year:d.getFullYear(),month:d.getMonth()+1,day:d.getDate(),hour:d.getHours(),minute:d.getMinutes(),lat:b.lat,lon:b.lon,tz:b.tz}; } export function fmtTime(h,m) { return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`; } export function dateToJD(s) { const[y,mo,d]=s.split('-').map(Number); return Date.UTC(y,mo-1,d)/86400000+2440587.5; } function getVargaLagna(ascSign, degInSign, vId) { const fake = {_L:{sign:ascSign,degree_in_sign:degInSign,house:1,error:false}}; return computeVarga(fake, vId)?.planets?._L?.sign || null; } function getAllVL(ascSign, degInSign) { const ids=['D1','D4','D7','D9','D10','D12','D24','D30','D60'], r={}; for(const id of ids) { const s = id==='D1' ? ascSign : getVargaLagna(ascSign, degInSign, id); r[id] = s ? {sign:s, index:SIGNS.indexOf(s)} : null; } return r; } function getNakPada(deg) { if(deg==null)return null; const nl=360/27,ni=Math.floor(deg/nl),p=Math.floor((deg-ni*nl)/(nl/4))+1; return {nakIdx:ni,pada:p,nakName:NAKSHATRA_LIST[ni]}; } // ——— Dasha 对齐评分 (40%) ——— function findActiveDasha(dr, evtDate) { const ejd=dateToJD(evtDate); for(const md of dr.timeline){ const s=dateToJD(md.start),e=dateToJD(md.end); if(ejd>=s&&ejd=dateToJD(ad.start)&&ejdgetHouseLord(ai,h)); const {mahadasha:md,antardasha:ad}=di; const mdOk=cat.planets.includes(md)||hl.includes(md); if(cat.planets.includes(md))sc+=3; else if(hl.includes(md))sc+=2; if(ad){ const adOk=cat.planets.includes(ad)||hl.includes(ad); if(cat.planets.includes(ad))sc+=2; else if(hl.includes(ad))sc+=1.5; if(mdOk&&adOk)sc+=2; } return sc; } // ——— 分盘一致性评分 (35%) ——— function scoreVarga(evt, vl, ascSign) { const cat=EVENT_CATEGORIES[evt.category]; if(!cat||!cat.varga)return 0; const v=vl[cat.varga]; if(!v)return 0; const lord=SIGN_LORDS[v.sign],ai=SIGNS.indexOf(ascSign); let sc=0; if(cat.planets.includes(lord))sc+=3; for(const h of cat.houses){if(getHouseLord(ai,h)===lord){sc+=2;break;}} const vh=((v.index-ai+12)%12)+1; if([1,4,5,7,9,10].includes(vh))sc+=1; return sc; } // ——— 宫位变化 (15%) ——— function detectHouseChanges(bc, nc) { const ch=[]; for(const pn of Object.keys(bc.planets)){ const bp=bc.planets[pn],np=nc.planets[pn]; if(bp.house!==np?.house) ch.push({planet:pn,from:bp.house,to:np?.house}); } return ch; } // ——— Nak Pada 变化 (10%) ——— function scoreNak(baseDeg, offDeg) { const bp=getNakPada(baseDeg),op=getNakPada(offDeg); if(!bp||!op)return 0; if(bp.nakIdx!==op.nakIdx)return 2; if(bp.pada!==op.pada)return 1; return 0; } function eventCategoryGroup(category) { return EVENT_COLLECTION_GUIDE.find(group => group.categories.includes(category))?.key || category; } function summarizeEventCoverage(events) { const valid = (events || []).filter(evt => evt?.date && EVENT_CATEGORIES[evt.category]); const groups = [...new Set(valid.map(evt => eventCategoryGroup(evt.category)))]; const categories = [...new Set(valid.map(evt => evt.category))]; const datedYears = valid.map(evt => Number(String(evt.date).slice(0, 4))).filter(Number.isFinite); const yearSpan = datedYears.length ? Math.max(...datedYears) - Math.min(...datedYears) : 0; const idealCount = valid.length >= 8; const enoughCount = valid.length >= 5; const enoughGroups = groups.length >= 3; const enoughSpan = yearSpan >= 7 || valid.length >= 8; const qualityScore = Math.round( Math.min(valid.length / 8, 1) * 40 + Math.min(groups.length / 4, 1) * 30 + Math.min(yearSpan / 12, 1) * 20 + Math.min(categories.length / 5, 1) * 10 ); const missing = EVENT_COLLECTION_GUIDE .filter(group => !groups.includes(group.key)) .slice(0, 3) .map(group => group.cn); return { event_count: valid.length, category_count: categories.length, group_count: groups.length, year_span: yearSpan, quality_score: qualityScore, quality_level: idealCount && enoughGroups && enoughSpan ? 'strong' : enoughCount && enoughGroups ? 'usable' : 'thin', missing_groups: missing, }; } function countMatchedEvents(result) { return (result?.eventScores || []).filter(es => (es.dashaScore || 0) > 0 || (es.vargaScore || 0) > 0).length; } function summarizeCandidateEvidence(result, events) { const eventCount = Math.max((events || []).length, 1); const matchedEvents = countMatchedEvents(result); const strongEvents = (result?.eventScores || []).filter(es => (es.dashaScore || 0) >= 4 || ((es.dashaScore || 0) > 0 && (es.vargaScore || 0) > 0)).length; const sensitiveVargas = (result?.vargaChanges || []) .filter(v => ['D9', 'D10', 'D24', 'D30', 'D60'].includes(v.varga)) .map(v => v.varga); const changedPlanets = (result?.houseChanges || []).map(c => c.planet); return { matched_events: matchedEvents, strong_events: strongEvents, match_rate: Math.round((matchedEvents / eventCount) * 100), sensitive_vargas: [...new Set(sensitiveVargas)], changed_planets: [...new Set(changedPlanets)], }; } function buildRectificationDecisionPlan(events) { const valid = (events || []).filter(evt => evt?.date && EVENT_CATEGORIES[evt.category]); const categorySet = [...new Set(valid.map(evt => evt.category))]; const themeVargas = []; for (const category of categorySet) { const vargas = RECTIFICATION_THEME_VARGAS[category] || []; for (const varga of vargas) { if (!themeVargas.includes(varga)) themeVargas.push(varga); } } const eventCount = valid.length; const tightenWithSensitive = themeVargas.length > 0; const useD30 = themeVargas.includes('D30'); const useD60 = eventCount >= 8 && themeVargas.length >= 2; return { principle: 'Dasha 定框,D9/D10 定核心,专项分盘补刀,D30/D60 谨慎后置。', ordered_layers: [ { step: 1, label: 'Dasha + dated events', role: '主引擎', reason: '先用真实事件把时间框住,再决定要不要细分盘。' }, { step: 2, label: 'D9', role: '核心验证', reason: '婚姻、关系质量、内在成熟路线的第一核心分盘。' }, { step: 3, label: 'D10', role: '核心验证', reason: '事业显化、职业兑现、升职和名声路径的第一核心分盘。' }, { step: 4, label: themeVargas.length ? themeVargas.join(' / ') : 'D7 / D12 / D24 / D4 / D2', role: '主题补刀', reason: '按用户真实事件主题调用专项分盘,不一股脑全开。' }, { step: 5, label: 'Nakshatra / Pada / house shifts', role: '细分钟收口', reason: '用于最后 2-10 分钟缩窄,不适合一开始拍板。' }, { step: 6, label: useD30 ? 'D30' : 'D30(仅当有健康/事故/创伤事件)', role: '高敏慎用', reason: '高敏但易过拟合,只在明确压力事件时启用。' }, { step: 7, label: useD60 ? 'D60(仅作最后参考)' : 'D60(默认后置)', role: '超高敏慎用', reason: '只在高收敛后作为最后参考,不反向主导校时结论。' }, ], selected_theme_vargas: themeVargas, event_count: eventCount, warnings: [ ...(eventCount < 5 ? ['事件少于 5 个时,不建议过早依赖高敏感分盘。'] : []), ...(!tightenWithSensitive ? ['当前事件主题不足以触发专项分盘,先补婚恋/事业/家庭/健康类 dated events。'] : []), ...(!useD30 ? ['没有明确健康/事故/创伤事件时,D30 默认不主导结论。'] : []), ...(!useD60 ? ['D60 默认后置,只在高收敛后作最后参考。'] : []), ], }; } export function buildRectificationInterviewQuestions() { return EVENT_COLLECTION_GUIDE.map(group => { const category = group.categories.find(key => EVENT_CATEGORIES[key]) || group.categories[0]; const cat = EVENT_CATEGORIES[category] || {}; return { id: `guided_rectification_interview_${group.key}`, group: group.key, category, varga: cat.varga || 'D1', label_cn: group.cn, label_en: group.en, question_cn: `你的人生中是否发生过日期比较明确的${group.cn}?`, question_en: `Have you had a dated ${group.en} event?`, examples_cn: group.categories .map(key => EVENT_CATEGORIES[key]?.cn) .filter(Boolean) .slice(0, 4), }; }); } export function buildRecommendedRectificationQuestions(recommendedEvents = []) { const picked = []; for (const key of recommendedEvents || []) { const question = RECTIFICATION_RECOMMENDED_EVENT_QUESTION_MAP[key]; if (question && !picked.find(item => item.id === question.id)) picked.push(question); } if (picked.length) return picked; return buildRectificationInterviewQuestions().slice(0, 3); } export function rectificationInterviewAnswersToEvents(answers) { return (answers || []) .filter(answer => answer?.answer === 'yes' && answer.date && EVENT_CATEGORIES[answer.category]) .map(answer => { const cat = EVENT_CATEGORIES[answer.category]; return { date: answer.date, category: answer.category, desc: (answer.note || '').trim() || cat.cn, source: 'guided_rectification_interview', }; }); } function buildRectificationAudit(best, results, events) { const second = results[1] || null; const coverage = summarizeEventCoverage(events); const evidence = summarizeCandidateEvidence(best, events); const cluster = results.filter(r => Math.abs((r.totalScore || 0) - (best?.totalScore || 0)) <= 3).slice(0, 8); const clusterOffsets = cluster.map(r => r.offsetMin); const clusterSpan = clusterOffsets.length ? { min: Math.min(...clusterOffsets), max: Math.max(...clusterOffsets), count: clusterOffsets.length } : { min: 0, max: 0, count: 0 }; const gap = second ? Math.max(0, (best.totalScore || 0) - (second.totalScore || 0)) : best?.totalScore || 0; const exactTie = second ? (best.totalScore || 0) === (second.totalScore || 0) : false; const shouldStayNearBaseline = exactTie || clusterSpan.count >= 4 || (best?.totalScore || 0) < 45; const warnings = []; if (coverage.event_count < 5) warnings.push('事件数量不足,建议至少补到5个,理想为8-15个。'); if (coverage.group_count < 3) warnings.push('事件类型过于集中,容易把结果锁到单一主题。'); if (coverage.year_span < 7 && coverage.event_count < 8) warnings.push('事件年份跨度偏短,建议补童年/青年/近年事件。'); if (exactTie) warnings.push('第一名与第二名同分,不应把单一候选时间视为确定校正。'); if (best?.vargaChanges?.some(v => v.varga === 'D9')) warnings.push('推荐时间会改变D9上升,婚姻与内在成熟判断需二次确认。'); return { coverage, evidence, runner_up: second ? { offsetMin: second.offsetMin, time: second.time, totalScore: second.totalScore } : null, score_gap: gap, exact_tie: exactTie, top_cluster: clusterSpan, should_stay_near_baseline: shouldStayNearBaseline, warnings, }; } // ——— 主函数 ——— export async function runRectification(birth, events, options={}) { const {rangeMin=15,stepMin=1,onProgress=null}=options; if(!events||!events.length) return {error:'请至少添加一个生命事件'}; const offsets=[]; for(let m=-rangeMin;m<=rangeMin;m+=stepMin) offsets.push(m); const results=[]; const baseChart=await computeChart(birth); const baseVL=getAllVL(baseChart.ascendant.sign,baseChart.ascendant.degree_in_sign); const baseMoon=baseChart.planets.Moon?.degree; for(let i=0;i0?dRaw/dMax:0, vPct=vMax>0?vRaw/vMax:0; const hPct=Math.min(hc.length/hMax,1), nPct=nRaw/2; const final=Math.round(dPct*40+vPct*35+hPct*15+nPct*10); results.push({ offsetMin:off,time:fmtTime(mod.hour,mod.minute),ascSign,ascDeg:chart.ascendant.degree, vargaLagnas:vl,vargaChanges:vc,houseChanges:hc,totalScore:final, scores:{ dasha:{raw:dRaw,max:dMax,pct:Math.round(dPct*100)}, varga:{raw:vRaw,max:vMax,pct:Math.round(vPct*100)}, house:{raw:hc.length,pct:Math.round(hPct*100)}, nak:{raw:nRaw,pct:Math.round(nPct*100)}, }, eventScores:evScores, moonNak:getNakPada(moonLon), }); }catch(e){console.warn('[Rect]',off,e);} if(onProgress)onProgress(i+1,offsets.length); } results.sort((a,b)=>(b.totalScore-a.totalScore)||Math.abs(a.offsetMin)-Math.abs(b.offsetMin)||a.offsetMin-b.offsetMin); if(results.length>0){ results[0].isBest=true; if(results.findIndex(r=>r.offsetMin===0)>0) results[0].correctionEffective=true; } const audit = results.length ? buildRectificationAudit(results[0], results, events) : null; const decisionPlan = buildRectificationDecisionPlan(events); return { birth,events,options:{rangeMin,stepMin,totalCandidates:offsets.length}, baseChartInfo:{ time:fmtTime(birth.hour,birth.minute), ascSign:baseChart.ascendant.sign,ascDeg:baseChart.ascendant.degree, vargaLagnas:baseVL,moonNak:getNakPada(baseMoon), }, results,bestMatch:results[0]||null, confidence:calcConf(results,events.length,audit), audit, decisionPlan, }; } function calcConf(results, evtCount, audit=null) { if(!results.length) return {level:'不确定',score:0,bestPct:0,gapPct:0,description:'',recommendation:[]}; const best=results[0],sec=results[1]; const gap=sec?best.totalScore-sec.totalScore:best.totalScore; const gapPct=sec&&sec.totalScore>0?Math.round(gap/sec.totalScore*100):100; let level,score; const coverageOk = !audit || audit.coverage.quality_level !== 'thin'; if(best.totalScore>=65&&gapPct>=15&&coverageOk&&!audit?.exact_tie){level='高';score=3;} else if(best.totalScore>=45&&gapPct>=8&&!audit?.exact_tie){level='中';score=2;} else if(best.totalScore>=25){level='低';score=1;} else{level='不确定';score=0;} const recs=[`建议出生时间校正 ${best.offsetMin>=0?'+':''}${best.offsetMin} 分钟`]; if(best.vargaChanges?.some(v=>v.varga==='D9')) recs.push('⚠️ 此校正会改变Navamsa(D9)上升,影响重大,建议专业验证'); if(audit?.should_stay_near_baseline) recs.push('候选时间仍有聚集或同分现象,建议先保留原始时间为主、把推荐时间作为验证候选。'); if(evtCount<5) recs.push('建议提供至少5个以上生命事件以提高准确性'); if(audit?.coverage?.missing_groups?.length) recs.push(`可优先补充:${audit.coverage.missing_groups.join('、')}。`); if(level==='高') recs.push('多个事件一致指向此时间,可信度较高'); else if(level==='低'||level==='不确定') recs.push('事件对齐度较低,可能需要更多事件数据或尝试更大时间范围'); return {level,score,bestPct:best.totalScore,gapPct,description:`最佳匹配评分 ${best.totalScore}%,领先第二名 ${gapPct}%`,recommendation:recs}; }