Files
Jyotisha/jyotish-app/rectification-engine.js
T
2026-07-01 19:15:35 +08:00

474 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 生时校正引擎 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<e){
if(md.antardasha) for(const ad of md.antardasha){
if(ejd>=dateToJD(ad.start)&&ejd<dateToJD(ad.end)) return {mahadasha:md.lord,antardasha:ad.lord};
}
return {mahadasha:md.lord,antardasha:null};
}
}
return null;
}
function scoreDasha(evt, di, ascSign) {
const cat=EVENT_CATEGORIES[evt.category]; if(!cat||!di)return 0;
let sc=0; const ai=SIGNS.indexOf(ascSign), hl=cat.houses.map(h=>getHouseLord(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;i<offsets.length;i++){
const off=offsets[i], mod=offsetBirth(birth,off);
try{
const chart=await computeChart(mod);
const ascSign=chart.ascendant.sign, moonLon=chart.planets.Moon?.degree;
if(!moonLon)continue;
const vl=getAllVL(ascSign,chart.ascendant.degree_in_sign);
const bs=`${mod.year}-${String(mod.month).padStart(2,'0')}-${String(mod.day).padStart(2,'0')}`;
const dashaRes=computeDasha(moonLon,bs,null);
let dRaw=0,vRaw=0,nRaw=0; const evScores=[];
for(const evt of events){
const di=findActiveDasha(dashaRes,evt.date);
const ds=scoreDasha(evt,di,ascSign), vs=scoreVarga(evt,vl,ascSign);
dRaw+=ds; vRaw+=vs;
evScores.push({event:evt,dasha:di,dashaScore:ds,vargaScore:vs});
}
nRaw=scoreNak(baseMoon,moonLon);
const hc=detectHouseChanges(baseChart,chart);
const vc=[];
for(const id of Object.keys(baseVL)){
if(baseVL[id]&&vl[id]&&baseVL[id].sign!==vl[id].sign)
vc.push({varga:id,from:baseVL[id].sign,to:vl[id].sign});
}
const dMax=events.length*9,vMax=events.length*6,hMax=Math.max(events.length,1);
const dPct=dMax>0?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};
}