Files
Jyotisha/scripts/jyotish_engine.py
T
linmeng f4e35974c6
Deploy staging to test server / deploy (push) Failing after 14m49s
Revert "merge: sync GitHub staging to Gitea"
This reverts commit a55c69115d, reversing
changes made to 02c9c9f3d6.
2026-07-30 14:38:17 +08:00

6324 lines
290 KiB
Python
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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
印度占星统一引擎 v3.7.1 (Jyotish Unified Engine)
整合所有计算能力为单一CLI入口,供Skill调用
子命令:
chart 计算完整星盘(基于Swiss Ephemeris
dasha 计算Vimshottari Dasha大运时间线
yoga Yoga格局识别
predict 三层验证法事件预测(优先EventPredictionModel规则引擎)
varga 分盘计算(D9/D10
varga-full BPHS十六分盘完整计算(D2-D60)(v3.7新增)
aspects 度数精确相位系统(v3.7新增)
jaimini Jaimini系统(Chara Karaka/KarakamshaChara Dasha timing covered)(v3.7新增)
nakshatra-adv 高级Nakshatra分析(Tara Bala/Sub-Lord/兼容性)(v3.7新增)
argala Argala门闩系统(v3.7新增)
tajika Tajika/Varshaphala年运盘(v3.7新增)
synastry 合盘分析(Ashta Koota 36分制+Mangal Dosha)(v3.7新增)
full-reading 全自动综合解盘(出生信息→全链路计算→完整报告)(v3.7.1新增)
celebrity 名人案例查询(15,807条数据+SQLite验证库)
db-stats 验证数据库统计
transit 行星过境查询
shadbala 六重力量计算(covered;外部绝对值校准前保留置信度上限)(v3.4新增)
ashtakavarga 八分法计算(v3.5升级BPHS完整表,SAV=337
memory Hermes记忆系统(v3.4新增)
validate R1-R10数学验证(v3.5新增,含R2b BAV列→SAV校验)
audit P1-P12行星审计管线(v3.6升级含P3仓库耦合+P8年龄状态+冲突仲裁)
report MD→HTML报告生成(v3.6新增,羊皮纸主题)
用法示例:
python3 jyotish_engine.py chart --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8
python3 jyotish_engine.py shadbala --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8
python3 jyotish_engine.py ashtakavarga --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8
python3 jyotish_engine.py memory --action store --content "测试记忆"
"""
import argparse
import json
import sys
import os
import csv
import math
import time
import sqlite3
import importlib.util
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from typing import Dict, List
try:
from tabulate import tabulate
except ModuleNotFoundError: # pragma: no cover - minimal environments
def tabulate(rows, headers=(), tablefmt=None):
lines = []
if headers:
lines.append(" | ".join(str(item) for item in headers))
lines.extend(" | ".join(str(item) for item in row) for row in rows)
return "\n".join(lines)
from life_stage_hook import generate_life_stage_hooks
from capability_evidence_pool import build_capability_evidence_pool_summary
from guided_topic_discovery import build_guided_topics
from ayanamsa_utils import (
AYANAMSA_DISPLAY_NAMES,
AYANAMSA_MODES,
apply_ayanamsa,
ayanamsa_display_name,
current_ayanamsa_name,
)
# ============================================================================
# 路径常量
# ============================================================================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.dirname(SCRIPT_DIR)
if ROOT_DIR not in sys.path:
sys.path.insert(0, ROOT_DIR)
HOME_DIR = os.path.expanduser('~')
CLAW_DIR = os.path.join(HOME_DIR, 'WorkBuddy', 'Claw')
DB_PATH = os.path.join(CLAW_DIR, 'vedic_astrology_validation.db')
PERSON_CSV = os.path.join(CLAW_DIR, 'vedastro_data', 'PersonList-15k.csv')
TRANSIT_JSON = os.path.join(CLAW_DIR, '月运过境配置-2026-2028.json')
try:
from local_env import load_local_env
except ModuleNotFoundError: # pragma: no cover - compatibility when imported as package
from scripts.local_env import load_local_env
load_local_env(ROOT_DIR)
try:
import swisseph as swe
apply_ayanamsa('lahiri', swe) # P0修复:必须设置Lahiri恒星黄道模式
HAS_SWE = True
except ImportError:
HAS_SWE = False
from cmd_solar_return import cmd_solar_return # v6.0.18
from cmd_narayana_dasha import cmd_narayana_dasha as _cmd_narayana_dasha_impl # v6.0.20
from cmd_muhurta import cmd_muhurta # v6.0.21
from yoga_engine import detect_yogas # v6.0.26: data-driven Yoga engine
from kp_system import calc_kp_analysis, get_kp_lords # v6.9.10: KP完整系统
from bhava_chalit import cmd_bhava_chalit # v6.9.13: Bhava Chalit 不等宫边界调整
from sudarshana_chakra import calc_sudarshana_chakra, generate_sudarshana_report # v6.9.14: Sudarshana Chakra 三参考点盘
# ============================================================================
# 常量
# ============================================================================
NAKSHATRA_LIST = [
("Ashwini", "Ketu", 7), ("Bharani", "Venus", 20), ("Krittika", "Sun", 6),
("Rohini", "Moon", 10), ("Mrigashira", "Mars", 7), ("Ardra", "Rahu", 18),
("Punarvasu", "Jupiter", 16), ("Pushya", "Saturn", 19), ("Ashlesha", "Mercury", 17),
("Magha", "Ketu", 7), ("Purva Phalguni", "Venus", 20), ("Uttara Phalguni", "Sun", 6),
("Hasta", "Moon", 10), ("Chitra", "Mars", 7), ("Swati", "Rahu", 18),
("Vishakha", "Jupiter", 16), ("Anuradha", "Saturn", 19), ("Jyeshtha", "Mercury", 17),
("Mula", "Ketu", 7), ("Purva Ashadha", "Venus", 20), ("Uttara Ashadha", "Sun", 6),
("Shravana", "Moon", 10), ("Dhanishta", "Mars", 7), ("Shatabhisha", "Rahu", 18),
("Purva Bhadrapada", "Jupiter", 16), ("Uttara Bhadrapada", "Saturn", 19), ("Revati", "Mercury", 17),
]
DASHA_ORDER = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"]
DASHA_YEARS = {"Ketu": 7, "Venus": 20, "Sun": 6, "Moon": 10, "Mars": 7, "Rahu": 18, "Jupiter": 16, "Saturn": 19, "Mercury": 17}
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
SIGNS_CN = {'Aries': '白羊座', 'Taurus': '金牛座', 'Gemini': '双子座', 'Cancer': '巨蟹座', 'Leo': '狮子座', 'Virgo': '处女座', 'Libra': '天秤座', 'Scorpio': '天蝎座', 'Sagittarius': '射手座', 'Capricorn': '摩羯座', 'Aquarius': '水瓶座', 'Pisces': '双鱼座'}
SIGN_LORDS = {'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon', 'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars', 'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'}
EXALTATION = {'Sun': 'Aries', 'Moon': 'Taurus', 'Mars': 'Capricorn', 'Mercury': 'Virgo', 'Jupiter': 'Cancer', 'Venus': 'Pisces', 'Saturn': 'Libra'}
DEBILITATION = {'Sun': 'Libra', 'Moon': 'Scorpio', 'Mars': 'Cancer', 'Mercury': 'Pisces', 'Jupiter': 'Capricorn', 'Venus': 'Virgo', 'Saturn': 'Aries'}
# Moolatrikona(三方本宫): 行星 → (星座, 度数范围起始)
MOOLATRIKONA = {'Sun': ('Leo', 0, 20), 'Moon': ('Taurus', 0, 3), 'Mars': ('Aries', 0, 12), 'Mercury': ('Virgo', 16, 20), 'Jupiter': ('Sagittarius', 0, 10), 'Venus': ('Libra', 0, 15), 'Saturn': ('Aquarius', 0, 20)}
# BPHS 永久友好关系表 (Naisargika Mitra)
PERMANENT_FRIENDS = {
'Sun': ['Moon', 'Mars', 'Jupiter'],
'Moon': ['Sun', 'Mercury'],
'Mars': ['Sun', 'Moon', 'Jupiter'],
'Mercury': ['Sun', 'Venus'],
'Jupiter': ['Sun', 'Moon', 'Mars'],
'Venus': ['Mercury', 'Saturn'],
'Saturn': ['Mercury', 'Venus'],
'Rahu': ['Venus', 'Saturn'],
'Ketu': ['Mars', 'Saturn'],
}
PERMANENT_ENEMIES = {
'Sun': ['Saturn', 'Venus'],
'Moon': [],
'Mars': ['Mercury'],
'Mercury': ['Moon'],
'Jupiter': ['Mercury', 'Venus'],
'Venus': ['Sun', 'Moon'],
'Saturn': ['Sun', 'Moon', 'Mars'],
'Rahu': ['Sun', 'Moon', 'Jupiter'],
'Ketu': ['Sun', 'Moon'],
}
DIGNITY_LABELS = {
'EXALTED': '入旺(Exalted)',
'MOOLATRIKONA': '本垣(Moolatrikona)',
'OWN_SIGN': '入庙(Own Sign)',
'GREAT_FRIEND': '极友(Great Friend)',
'FRIEND': '入友(Friendly Sign)',
'NEUTRAL': '中性(Neutral)',
'ENEMY': '入敌(Enemy Sign)',
'GREAT_ENEMY': '极敌(Great Enemy)',
'DEBILITATED': '落陷(Debilitated)',
'NEECHA_BHANGA': '落陷取消(Neecha Bhanga)',
}
def _build_vimsopaka_semantic_summary(vimsopaka: dict | None) -> dict:
if not isinstance(vimsopaka, dict):
return {"status": "blocked", "highlights": [], "warnings": []}
highlights = []
warnings = []
for planet, payload in vimsopaka.items():
if not isinstance(payload, dict):
continue
dignity = payload.get('dignity')
label = DIGNITY_LABELS.get(dignity)
if dignity in {'GREAT_FRIEND', 'NEECHA_BHANGA'} and label:
highlights.append(f"{planet}: {label}")
elif dignity == 'GREAT_ENEMY' and label:
warnings.append(f"{planet}: {label}")
return {
"status": "used",
"highlights": highlights,
"warnings": warnings,
}
PUSHKARA_NAVAMSA_RANGES = {
'fire': [(6 + 40/60, 10), (23 + 20/60, 26 + 40/60)],
'earth': [(3 + 20/60, 6 + 40/60), (16 + 40/60, 20)],
'air': [(13 + 20/60, 16 + 40/60), (26 + 40/60, 30)],
'water': [(0, 3 + 20/60), (10, 13 + 20/60)],
}
PUSHKARA_BHAGA_DEGREES = {'fire': 21, 'earth': 14, 'air': 24, 'water': 7}
SIGN_ELEMENTS = {
'Aries': 'fire', 'Leo': 'fire', 'Sagittarius': 'fire',
'Taurus': 'earth', 'Virgo': 'earth', 'Capricorn': 'earth',
'Gemini': 'air', 'Libra': 'air', 'Aquarius': 'air',
'Cancer': 'water', 'Scorpio': 'water', 'Pisces': 'water',
}
def _element_key(sign):
return SIGN_ELEMENTS.get(sign)
def _is_pushkara_navamsa(sign, deg_in_sign):
element = _element_key(sign)
if element is None or deg_in_sign is None:
return False, None
for start, end in PUSHKARA_NAVAMSA_RANGES[element]:
if start <= deg_in_sign < end:
return True, {"element": element, "range": [round(start, 4), round(end, 4)]}
return False, None
def _is_pushkara_bhaga(sign, deg_in_sign, orb=1.0):
element = _element_key(sign)
if element is None or deg_in_sign is None:
return False, None
target = PUSHKARA_BHAGA_DEGREES[element]
delta = abs(deg_in_sign - target)
return delta <= orb, {"element": element, "target_degree": target, "orb": orb, "delta": round(delta, 4)}
def _calc_vargottama(planets, varga_full):
d9 = varga_full.get('D9_Navamsa', {}) if isinstance(varga_full, dict) else {}
result = {}
for pn, pd in planets.items():
if not isinstance(pd, dict) or 'sign' not in pd:
continue
d9_pd = d9.get(pn, {}) if isinstance(d9, dict) else {}
d9_sign = d9_pd.get('sign') if isinstance(d9_pd, dict) else None
result[pn] = {
'd1_sign': pd.get('sign'),
'd9_sign': d9_sign,
'is_vargottama': bool(d9_sign and pd.get('sign') == d9_sign),
}
return result
def _calc_pushkara_flags(planets):
result = {}
for pn, pd in planets.items():
if not isinstance(pd, dict) or 'sign' not in pd:
continue
sign = pd.get('sign')
deg = pd.get('degree_in_sign', pd.get('degree', 0) % 30)
in_pna, pna_meta = _is_pushkara_navamsa(sign, deg)
in_pb, pb_meta = _is_pushkara_bhaga(sign, deg)
result[pn] = {
'sign': sign,
'sign_cn': SIGNS_CN.get(sign, ''),
'degree_in_sign': round(deg, 4),
'pushkara_navamsa': in_pna,
'pushkara_navamsa_meta': pna_meta,
'pushkara_bhaga': in_pb,
'pushkara_bhaga_meta': pb_meta,
}
return result
def _calc_sensitive_points(planets):
result = {}
try:
from varga import calc_bhrigu_bindu, calc_sarpa_drekkana
except Exception:
return result
moon = planets.get('Moon', {}) if isinstance(planets, dict) else {}
rahu = planets.get('Rahu', {}) if isinstance(planets, dict) else {}
moon_lon = moon.get('degree_raw', moon.get('degree'))
rahu_lon = rahu.get('degree_raw', rahu.get('degree'))
if moon_lon is not None and rahu_lon is not None:
result['bhrigu_bindu'] = calc_bhrigu_bindu(moon_lon, rahu_lon)
sarpa = {}
for planet_name, pdata in planets.items():
if not isinstance(pdata, dict):
continue
lon = pdata.get('degree_raw', pdata.get('degree'))
if lon is None:
continue
payload = calc_sarpa_drekkana(lon)
if payload.get('is_sarpa_drekkana'):
sarpa[planet_name] = payload
result['sarpa_drekkana'] = sarpa
return result
def _calc_dasha_sandhi(dasha_result, reference_date=None, orb_days=90):
ref = datetime.strptime(reference_date, "%Y-%m-%d") if reference_date else datetime.now()
sandhi = []
timeline = dasha_result.get('timeline', []) if isinstance(dasha_result, dict) else []
for md in timeline:
for boundary_key in ['start', 'end']:
if boundary_key not in md:
continue
bdt = datetime.strptime(md[boundary_key], "%Y-%m-%d")
delta = (bdt - ref).days
if abs(delta) <= orb_days:
sandhi.append({
'level': 'mahadasha',
'lord': md.get('lord'),
'boundary': boundary_key,
'date': md.get(boundary_key),
'days_from_reference': delta,
'within_orb': True,
})
for ad in md.get('antardasha_timeline', []):
for boundary_key in ['start', 'end']:
if boundary_key not in ad:
continue
bdt = datetime.strptime(ad[boundary_key], "%Y-%m-%d")
delta = (bdt - ref).days
if abs(delta) <= orb_days:
sandhi.append({
'level': 'antardasha',
'mahadasha_lord': md.get('lord'),
'lord': ad.get('lord'),
'boundary': boundary_key,
'date': ad.get(boundary_key),
'days_from_reference': delta,
'within_orb': True,
})
return {'reference_date': ref.strftime('%Y-%m-%d'), 'orb_days': orb_days, 'sandhi_windows': sandhi}
# ============================================================================
# 新增:Dispositor Chain + Inter-chart Linkage (v6.0.12)
# ============================================================================
def calc_dispositor_chain(planet_name, planets_data, max_depth=12):
"""
计算行星的定位星链(dispositor chain)。
返回列表:[{planet, sign, dispositor, dispositor_sign}, ...]
直到循环或达到 max_depth。
"""
chain = []
current_planet = planet_name
visited = set()
for _ in range(max_depth):
if current_planet in visited:
break
visited.add(current_planet)
pdata = planets_data.get(current_planet, {})
if not isinstance(pdata, dict) or 'sign' not in pdata:
break
sign = pdata['sign']
dispositor = SIGN_LORDS.get(sign, '')
chain.append({
'planet': current_planet,
'sign': sign,
'sign_cn': SIGNS_CN.get(sign, ''),
'dispositor': dispositor,
'dispositor_sign': sign, # 定位星所在的星座(即当前行星所在星座)
})
if not dispositor:
break
current_planet = dispositor
return chain
def calc_inter_chart_linkage(planet_name, d1_data, d9_data, d10_data, d12_data=None, d1_asc_idx=None, d9_asc_idx=None, d10_asc_idx=None):
"""
计算行星在 D1/D9/D10 分盘之间的飞星落宫。
返回 {chart_type: {sign, house_in_chart, lord_of_sign_in_chart}}
"""
result = {}
charts = [
('D1', d1_data, d1_asc_idx),
('D9', d9_data, d9_asc_idx),
('D10', d10_data, d10_asc_idx),
]
if d12_data:
charts.append(('D12', d12_data, None))
for chart_name, chart_data, asc_idx in charts:
if not isinstance(chart_data, dict):
continue
pdata = chart_data.get(planet_name, {})
if not isinstance(pdata, dict) or 'sign' not in pdata:
continue
sign = pdata['sign']
sign_idx = SIGNS.index(sign) if sign in SIGNS else 0
house = None
if asc_idx is not None:
house = ((sign_idx - asc_idx) % 12) + 1
lord = SIGN_LORDS.get(sign, '')
result[chart_name] = {
'planet': planet_name,
'sign': sign,
'sign_cn': SIGNS_CN.get(sign, ''),
'house': house,
'lord': lord,
}
return result
def calc_all_dispositor_chains(planets_data):
"""为所有行星计算定位星链"""
result = {}
for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu']:
if pname in planets_data:
result[pname] = calc_dispositor_chain(pname, planets_data)
return result
def calc_all_inter_chart_linkages(d1_data, d9_data, d10_data, d12_data=None, d1_asc_idx=None, d9_asc_idx=None, d10_asc_idx=None):
"""
为所有行星计算 D1/D9/D10 分盘间飞星。
需要各分盘的 asc_idx 来正确计算落宫。
"""
result = {}
for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu']:
result[pname] = calc_inter_chart_linkage(
pname, d1_data, d9_data, d10_data, d12_data,
d1_asc_idx, d9_asc_idx, d10_asc_idx
)
return result
def _get_temporary_relationship(planet1, planet2, planets_data):
"""Calculate Temporary Friendship (Tatkalika Maitri)."""
if not planets_data or planet1 not in planets_data or planet2 not in planets_data:
return 'NEUTRAL'
p1_sign = planets_data[planet1].get('sign')
p2_sign = planets_data[planet2].get('sign')
if not p1_sign or not p2_sign:
return 'NEUTRAL'
idx1 = SIGNS.index(p1_sign)
idx2 = SIGNS.index(p2_sign)
distance = (idx2 - idx1) % 12 + 1
# 2, 3, 4, 10, 11, 12 from planet are temporary friends
if distance in [2, 3, 4, 10, 11, 12]:
return 'FRIEND'
# 1 (conjunct), 5, 6, 7, 8, 9 are temporary enemies
else:
return 'ENEMY'
def _check_neecha_bhanga(planet, sign, planets_data):
"""Check for Cancellation of Debilitation."""
if not planets_data:
return False
sign_lord = SIGN_LORDS.get(sign)
# Condition 1: Dispositor is exalted.
if sign_lord and sign_lord in planets_data:
lord_sign = planets_data[sign_lord].get('sign')
if lord_sign and EXALTATION.get(sign_lord) == lord_sign:
return True
# Condition 2: Planet exalted in this sign is conjunct.
exalted_planet = None
for p, ex_sign in EXALTATION.items():
if ex_sign == sign:
exalted_planet = p
break
if exalted_planet and exalted_planet in planets_data:
ex_p_sign = planets_data[exalted_planet].get('sign')
if ex_p_sign == sign:
return True
return False
def _build_dignity_context(chart_data):
"""Normalize a chart/varga payload into the per-planet context used by dignity checks."""
context = {}
if not isinstance(chart_data, dict):
return context
for pn, pd in chart_data.items():
if pn in ('_meta', 'Ascendant') or not isinstance(pd, dict) or 'sign' not in pd:
continue
context[pn] = {
'sign': pd.get('sign'),
'degree_in_sign': pd.get('degree_in_sign', pd.get('degree', 0) % 30),
}
return context
def _get_dignity_level(planet, sign, deg_in_sign=None, planets_data=None):
"""判断行星在某星座的尊严等级 (用于 Vimsopaka 映射), 包含五重敌友和落陷取消"""
if EXALTATION.get(planet) == sign:
return 'EXALTED'
mt = MOOLATRIKONA.get(planet)
if mt and mt[0] == sign:
if deg_in_sign is not None and mt[1] <= deg_in_sign < mt[2]:
return 'MOOLATRIKONA'
if SIGN_LORDS.get(sign) == planet:
return 'OWN_SIGN'
if DEBILITATION.get(planet) == sign:
if _check_neecha_bhanga(planet, sign, planets_data):
return 'NEECHA_BHANGA'
return 'DEBILITATED'
sign_lord = SIGN_LORDS.get(sign, '')
if not sign_lord:
return 'NEUTRAL'
# Naisargika (Permanent)
perm = 'NEUTRAL'
if sign_lord in PERMANENT_FRIENDS.get(planet, []):
perm = 'FRIEND'
elif sign_lord in PERMANENT_ENEMIES.get(planet, []):
perm = 'ENEMY'
if not planets_data:
return perm
# Tatkalika (Temporary)
temp = _get_temporary_relationship(planet, sign_lord, planets_data)
# Panchadha Maitri (Compound)
if perm == 'FRIEND' and temp == 'FRIEND':
return 'GREAT_FRIEND'
elif perm == 'ENEMY' and temp == 'ENEMY':
return 'GREAT_ENEMY'
elif perm == 'FRIEND' and temp == 'ENEMY':
return 'NEUTRAL'
elif perm == 'NEUTRAL' and temp == 'FRIEND':
return 'FRIEND'
elif perm == 'NEUTRAL' and temp == 'ENEMY':
return 'ENEMY'
elif perm == 'ENEMY' and temp == 'FRIEND':
return 'NEUTRAL'
return 'NEUTRAL'
def _get_planet_status_label(planet, sign, deg_in_sign=None, planets_data=None):
"""Return the user-facing D1 dignity label for chart output."""
return DIGNITY_LABELS.get(_get_dignity_level(planet, sign, deg_in_sign, planets_data), '中性')
PLANET_CN = {"Ketu": "南交点Ketu", "Venus": "金星Venus", "Sun": "太阳Sun", "Moon": "月亮Moon", "Mars": "火星Mars", "Rahu": "北交点Rahu", "Jupiter": "木星Jupiter", "Saturn": "土星Saturn", "Mercury": "水星Mercury"}
BASE_PLANETS_SWE = {'Sun': swe.SUN, 'Moon': swe.MOON, 'Mars': swe.MARS, 'Mercury': swe.MERCURY, 'Jupiter': swe.JUPITER, 'Venus': swe.VENUS, 'Saturn': swe.SATURN} if HAS_SWE else {}
PLANETS_SWE = {**BASE_PLANETS_SWE, 'Rahu': swe.MEAN_NODE} if HAS_SWE else {}
def _node_pid(node_mode='mean'):
"""返回 Rahu/Ketu 节点计算口径对应的 Swiss Ephemeris id。"""
if not HAS_SWE:
return None
return swe.TRUE_NODE if (node_mode or 'mean').lower() == 'true' else swe.MEAN_NODE
def _planet_map_for_node_mode(node_mode='mean'):
"""返回七曜+Rahu 的行星映射,Rahu 口径由 node_mode 决定。"""
if not HAS_SWE:
return {}
return {**BASE_PLANETS_SWE, 'Rahu': _node_pid(node_mode)}
def _calc_sidereal_planets_for_jd(jd, node_mode='mean', include_ketu=True):
"""使用 Swiss Ephemeris 计算指定 Julian Day 的恒星黄道行星位置。"""
if not HAS_SWE:
return {}, None
ayanamsa = swe.get_ayanamsa(jd)
data = {}
for pname, pid in _planet_map_for_node_mode(node_mode).items():
pos, ret = swe.calc_ut(jd, pid)
if ret < 0:
continue
lon = (pos[0] - ayanamsa) % 360
sign_idx = int(lon / 30) % 12
speed = pos[3] if len(pos) > 3 else 0
data[pname] = {
'sign': SIGNS[sign_idx],
'sign_cn': SIGNS_CN[SIGNS[sign_idx]],
'degree': round(lon, 4),
'degree_raw': lon,
'degree_in_sign': round(lon % 30, 2),
'degree_in_sign_raw': lon % 30,
'speed': round(speed, 4),
'retrograde': speed < 0,
}
if include_ketu and 'Rahu' in data:
rahu_lon = data['Rahu']['degree_raw']
ketu_lon = (rahu_lon + 180) % 360
ketu_idx = int(ketu_lon / 30) % 12
data['Ketu'] = {
'sign': SIGNS[ketu_idx],
'sign_cn': SIGNS_CN[SIGNS[ketu_idx]],
'degree': round(ketu_lon, 4),
'degree_raw': ketu_lon,
'degree_in_sign': round(ketu_lon % 30, 2),
'degree_in_sign_raw': ketu_lon % 30,
'speed': data['Rahu'].get('speed', 0),
'retrograde': data['Rahu'].get('retrograde', True),
}
return data, ayanamsa
def output_json(data):
"""统一JSON输出"""
print(json.dumps(data, ensure_ascii=False, indent=2, default=str))
def output_table(command, data):
"""Human-readable ASCII table output for selected commands."""
if command == 'chart':
birth_info = data.get('birth_info', {}) if isinstance(data, dict) else {}
ascendant = data.get('ascendant', {}) if isinstance(data, dict) else {}
planets = data.get('planets', {}) if isinstance(data, dict) else {}
rows = []
for planet_name, planet_data in planets.items():
if not isinstance(planet_data, dict):
continue
rows.append([
planet_name,
planet_data.get('sign', ''),
planet_data.get('house', ''),
planet_data.get('nakshatra', ''),
planet_data.get('status', ''),
])
print(f"Birth: {birth_info.get('date', '')} {birth_info.get('time', '')}")
print(f"Ascendant: {ascendant.get('sign', '')} {ascendant.get('degree', '')}")
print(tabulate(rows, headers=['Planet', 'Sign', 'House', 'Nakshatra', 'Status'], tablefmt='github'))
return
if command == 'dasha':
timeline = data.get('timeline', []) if isinstance(data, dict) else []
current = data.get('current_dasha') if isinstance(data, dict) else {}
rows = []
for item in timeline:
rows.append([
item.get('lord', ''),
item.get('start', ''),
item.get('end', ''),
item.get('years', ''),
'yes' if item.get('is_current') else '',
])
print(f"Moon Nakshatra: {data.get('moon_nakshatra', '')}")
print(f"Birth Date: {data.get('birth_date', '')}")
print(f"Reference Date: {data.get('reference_date', '')}")
if isinstance(current, dict) and current:
print(
"Current: "
f"{current.get('lord', '')}"
f" ({current.get('start', '')} -> {current.get('end', '')})"
)
print(tabulate(rows, headers=['Mahadasha', 'Start', 'End', 'Years', 'Current'], tablefmt='github'))
return
if command == 'shadbala':
planets = data.get('planets', {}) if isinstance(data, dict) else {}
rows = []
for planet_name, planet_data in planets.items():
if not isinstance(planet_data, dict):
continue
rows.append([
planet_name,
planet_data.get('total_rupas', ''),
planet_data.get('min_required', ''),
planet_data.get('strength_level', ''),
planet_data.get('rank', ''),
])
print(f"Shadbala Method: {data.get('method', '')}")
print(tabulate(rows, headers=['Planet', 'Rupas', 'Min Required', 'Strength', 'Rank'], tablefmt='github'))
return
if command == 'ashtakoot':
scores = data.get('scores', {}) if isinstance(data, dict) else {}
rows = [[kuta, score] for kuta, score in scores.items()]
print(f"Ashtakoot Method: {data.get('method', '')}")
print(
f"Total Score: {data.get('total_score', '')}/{data.get('max_score', '')} | "
f"Match Approved: {data.get('is_match_approved', '')}"
)
print(tabulate(rows, headers=['Kuta', 'Score'], tablefmt='github'))
return
if command == 'ashtakavarga':
sav = data.get('sav', {}) if isinstance(data, dict) else {}
assessment = sav.get('assessment', []) if isinstance(sav, dict) else []
bav_validation = data.get('bav_validation', []) if isinstance(data, dict) else []
sign_rows = []
for item in assessment:
sign_rows.append([
item.get('sign', ''),
item.get('score', ''),
item.get('level', ''),
])
validation_rows = []
for item in bav_validation:
validation_rows.append([
item.get('planet', ''),
item.get('actual', ''),
item.get('expected', ''),
item.get('status', ''),
])
print(f"Ashtakavarga Method: {data.get('method', '')}")
print(
f"SAV Total: {sav.get('total', '')}/{sav.get('expected_total', '')} | "
f"Valid: {sav.get('valid', '')} | "
f"Strongest Signs: {', '.join(data.get('strongest_signs', []))}"
)
print(tabulate(sign_rows, headers=['Sign', 'Score', 'Level'], tablefmt='github'))
if validation_rows:
print()
print("BAV Validation:")
print(tabulate(validation_rows, headers=['Planet', 'Actual', 'Expected', 'Status'], tablefmt='github'))
return
output_json(data)
def _second_arg(value):
"""Validate CLI birth second values."""
second = int(value)
if second < 0 or second > 59:
raise argparse.ArgumentTypeError("second must be between 0 and 59")
return second
def _arg_second(args):
"""Return an argparse namespace's optional birth second."""
return int(getattr(args, 'second', 0) or 0)
def _birth_hour_decimal(hour, minute, second=0):
return hour + minute / 60.0 + second / 3600.0
def _birth_time_string(hour, minute, second=0):
second = int(second or 0)
if second:
return f"{int(hour):02d}:{int(minute):02d}:{second:02d}"
return f"{int(hour):02d}:{int(minute):02d}"
def _birth_datetime_from_args(args):
return datetime(args.year, args.month, args.day, args.hour, args.minute, _arg_second(args))
def _compute_chart_from_args(args):
from domain_calculation_service import compute_chart
result = compute_chart({
'year': args.year,
'month': args.month,
'day': args.day,
'hour': args.hour,
'minute': args.minute,
'second': _arg_second(args),
'lat': args.lat,
'lon': args.lon,
'tz': args.tz,
'node_mode': getattr(args, 'node_mode', 'mean'),
'ayanamsa': _current_ayanamsa_name(args),
})
asc_idx = SIGNS.index(result['ascendant']['sign'])
birth = result['birth_info']
return result, asc_idx, birth['julian_day'], birth['ayanamsa']
def _current_ayanamsa_name(args=None):
return current_ayanamsa_name(args)
def _ayanamsa_display_name(name):
return ayanamsa_display_name(name)
def _add_chart_args(p):
"""为需要出生数据的子命令添加公共参数"""
p.add_argument('--year', type=int, required=True)
p.add_argument('--month', type=int, required=True)
p.add_argument('--day', type=int, required=True)
p.add_argument('--hour', type=int, required=True)
p.add_argument('--minute', type=int, required=True)
p.add_argument('--second', type=_second_arg, default=0)
p.add_argument('--lat', type=float, required=True)
p.add_argument('--lon', type=float, required=True)
p.add_argument('--tz', type=float, default=None)
p.add_argument('--node-mode', default='mean', choices=['mean', 'true'], help='Rahu/Ketu节点口径:mean=Mean Node(默认,JHora常用/Swiss direct baseline),true=True NodePyJHora默认)')
p.add_argument('--ayanamsa', default='lahiri', choices=list(AYANAMSA_MODES.keys()),
help='恒星黄道系统(默认lahiri)。可选: raman, kp(krishnamurti), fagan_bradley, djwhal_khul, sassanian, true_citra')
def _apply_ayanamsa(ayanamsa_name):
"""应用指定的 Ayanamsa 系统到全局 swissph 设置。新增 v6.9.9"""
return bool(HAS_SWE and apply_ayanamsa(ayanamsa_name, swe))
def _varga_chart_to_yoga_context(varga_chart):
"""Convert varga.py output into yoga_engine context chart shape."""
if not isinstance(varga_chart, dict):
return {}
asc = varga_chart.get('Ascendant', {})
asc_sign = asc.get('sign') if isinstance(asc, dict) else None
asc_idx = SIGNS.index(asc_sign) if asc_sign in SIGNS else None
planets_ctx = {}
for pn, pd in varga_chart.items():
if pn in ('Ascendant', '_meta', '_dignity', '_d9_analysis', '_d60_analysis'):
continue
if not isinstance(pd, dict) or 'sign' not in pd:
continue
sign = pd.get('sign')
sign_idx = pd.get('sign_idx')
if sign_idx is None and sign in SIGNS:
sign_idx = SIGNS.index(sign)
house = None
if asc_idx is not None and sign_idx is not None:
house = ((int(sign_idx) - asc_idx) % 12) + 1
planets_ctx[pn] = {
'sign': sign,
'house': house,
'degree': pd.get('degree_in_sign', pd.get('degree')),
'degree_in_sign': pd.get('degree_in_sign', pd.get('degree')),
'sign_idx': sign_idx,
}
return {
'ascendant': asc_sign,
'ascendant_degree': asc.get('degree_in_sign') if isinstance(asc, dict) else None,
'planets': planets_ctx,
}
def _build_yoga_context_from_vargas(varga_result, planet_lons=None):
"""Build optional YogaContext payload with D9/D60 and basic panchanga."""
context = {}
if isinstance(varga_result, dict):
d9 = varga_result.get('D9_Navamsa') or {}
d60 = varga_result.get('D60_Shashtiamsa') or varga_result.get('D60_Shashtyamsa') or {}
if d9:
context['d9'] = _varga_chart_to_yoga_context(d9)
if d60:
context['d60'] = _varga_chart_to_yoga_context(d60)
if isinstance(planet_lons, dict) and 'Moon' in planet_lons and 'Sun' in planet_lons:
tithi_no = int(((planet_lons.get('Moon', 0) - planet_lons.get('Sun', 0)) % 360) / 12) + 1
context['panchanga'] = {
'tithi': tithi_no,
'paksha': 'waxing' if 1 <= tithi_no <= 15 else 'waning',
'is_waning_moon': tithi_no > 15,
}
return context
def _planet_snapshot(planets, planet_name):
pdata = planets.get(planet_name, {}) if isinstance(planets, dict) else {}
if not isinstance(pdata, dict):
return {}
return {
'source': pdata.get('source'),
'sign': pdata.get('sign'),
'sign_cn': pdata.get('sign_cn'),
'house': pdata.get('house'),
'degree': pdata.get('degree'),
'lon': pdata.get('lon'),
'degree_in_sign': pdata.get('degree_in_sign'),
'vargas': pdata.get('vargas'),
'nakshatra': pdata.get('nakshatra'),
'nakshatra_pada': pdata.get('nakshatra_pada'),
'status': pdata.get('status'),
'retrograde': pdata.get('retrograde'),
}
def _oracle_progress_snapshot():
try:
import json
import subprocess
import sys
import tempfile
from pathlib import Path
root = Path(__file__).resolve().parents[1]
oracle_file = root / 'references' / 'oracle' / 'dasha_shadbala_oracle_cases.json'
with tempfile.TemporaryDirectory() as temp_dir:
queue_path = Path(temp_dir) / 'oracle-queue.json'
queue = subprocess.run(
[sys.executable, 'scripts/oracle_collection_queue.py', '--oracle-file', str(oracle_file), '--format', 'json'],
cwd=root,
text=True,
capture_output=True,
timeout=90,
check=False,
)
if queue.returncode != 0:
raise RuntimeError(queue.stderr.strip() or queue.stdout.strip())
queue_path.write_text(queue.stdout, encoding='utf-8')
validation = subprocess.run(
[sys.executable, 'scripts/oracle_evidence_validator.py', '--queue-file', str(queue_path)],
cwd=root,
text=True,
capture_output=True,
timeout=90,
check=False,
)
if validation.returncode != 0:
raise RuntimeError(validation.stderr.strip() or validation.stdout.strip())
report = json.loads(validation.stdout)
summary = report.get('summary', {})
return {
'scope': 'external_oracle_evidence_validation',
'collection_queue': 'external_oracle_collection_queue',
'total_packets': summary.get('total_packets', 0),
'valid_packets': summary.get('valid_packets', 0),
'ready_for_calibration': summary.get('ready_for_calibration', 0),
'production_tuning_allowed': summary.get('production_tuning_allowed', False),
'artifact_policy': 'references/oracle/artifacts/',
'promotion_rule': 'external_verified requires source_artifact, filled target values, and non-local-engine external evidence.',
'boundary': report.get('boundary', 'Dasha/Shadbala absolute values are not externally calibrated until enough packets pass validation.'),
}
except Exception as exc:
return {
'scope': 'external_oracle_evidence_validation',
'collection_queue': 'external_oracle_collection_queue',
'total_packets': 0,
'valid_packets': 0,
'ready_for_calibration': 0,
'production_tuning_allowed': False,
'artifact_policy': 'references/oracle/artifacts/',
'promotion_rule': 'external_verified requires source_artifact, filled target values, and non-local-engine external evidence.',
'boundary': f'Oracle progress unavailable: {exc}',
}
def _functional_benefic_malefic_snapshot(planets, ascendant):
try:
from functional_benefics import derive_functional_benefic_malefic
asc_sign = ascendant.get('sign') if isinstance(ascendant, dict) else None
return derive_functional_benefic_malefic(asc_sign)
except Exception as exc:
return {
'status': 'blocked',
'ascendant': ascendant.get('sign') if isinstance(ascendant, dict) else None,
'functional_benefics': [],
'functional_malefics': [],
'functional_neutrals': [],
'yogakarakas': [],
'owned_houses': {},
'effect_on_confidence': f'未完成功能性吉凶星判定,需降低高严谨结论置信度: {exc}',
'source': 'strict_functional_benefic_malefic_v1',
}
def _build_technique_audit_table(functional_layer, oracle_progress, modules):
dasha = modules.get('dasha') if isinstance(modules, dict) else {}
narayana = modules.get('narayana_dasha') if isinstance(modules, dict) else {}
d9 = modules.get('d9_navamsa_expanded') if isinstance(modules, dict) else {}
shadbala = modules.get('shadbala') if isinstance(modules, dict) else {}
ashtakavarga = modules.get('ashtakavarga') if isinstance(modules, dict) else {}
vimsopaka = modules.get('vimsopaka') if isinstance(modules, dict) else {}
dasa_convergence = modules.get('dasa_convergence') if isinstance(modules, dict) else {}
relationship = modules.get('relationship_strict_evidence') if isinstance(modules, dict) else {}
vedastro_overview = modules.get('vedastro_range_scan_result') if isinstance(modules, dict) else {}
rows = [
{
'technique': 'Functional Benefic/Malefic',
'status': functional_layer.get('status', 'blocked'),
'source': 'scripts/yoga_engine.py::YogaContext',
'note': (
f"关键功能吉星={functional_layer.get('functional_benefics', [])}; "
f"关键功能凶星={functional_layer.get('functional_malefics', [])}; "
f"功能中性星={functional_layer.get('functional_neutrals', [])}; "
f"Yogakaraka={functional_layer.get('yogakarakas', [])}; "
f"{functional_layer.get('effect_on_confidence', '高严谨模式缺少功能性吉凶星判定。')}"
),
},
{
'technique': 'External Oracle Progress',
'status': 'used' if oracle_progress.get('total_packets', 0) else 'blocked',
'source': 'scripts/oracle_collection_queue.py + scripts/oracle_evidence_validator.py',
'note': (
f"当前 external oracle 进度 {oracle_progress.get('ready_for_calibration', 0)}/"
f"{oracle_progress.get('total_packets', 0)} ready"
f"production_tuning_allowed={oracle_progress.get('production_tuning_allowed', False)}"
),
},
{
'technique': 'Interpretation Source Pack',
'status': 'used',
'source': 'repo_existing_interpretation_sources',
'note': (
'已显式索引 interpretation_template_registry、P1-P12、house_framework、'
'Raman/BPHS 与前端 planet-house-details;这些资料只作为本地解释源,'
'不替代 MEVG 外部采集和真实案例校正。'
),
},
{
'technique': 'MEVG / Global Web Evidence',
'status': 'blocked',
'source': 'references/mandatory-verification-gate-protocol.md',
'note': '所有星盘运势/推运解释必须执行全球/全网外部资料采集;未执行时解释声明需降级。',
},
{
'technique': 'Real Case Calibration',
'status': 'blocked',
'source': 'references/real-reading-quality-checklist.md',
'note': '所有星盘运势/推运解释必须参考真实案例或公开 benchmark;无匹配案例时置信度封顶。',
},
]
rows.append({
'technique': 'Vimshottari + Narayana Cross-check',
'status': 'used' if isinstance(dasha, dict) and isinstance(narayana, dict) and dasha and narayana else 'blocked',
'source': 'modules.dasha + modules.narayana_dasha + modules.dasa_convergence',
'note': (
f"Vimshottari 当前主运={((dasha.get('current_dasha') or {}).get('lord')) if isinstance(dasha, dict) else None}; "
f"Narayana 当前主运={narayana.get('current_dasha') if isinstance(narayana, dict) else None}; "
f"多系统收敛摘要={dasa_convergence.get('top_convergent_domains') if isinstance(dasa_convergence, dict) else None}"
),
})
rows.append({
'technique': 'Relevant Vargas',
'status': 'used' if isinstance(d9, dict) and d9 else 'blocked',
'source': 'modules.d9_navamsa_expanded',
'note': (
f"D9 已接入;可见键={list(d9.keys())[:5] if isinstance(d9, dict) else []}"
"高严谨解读至少应交叉 D1 + D9,婚姻/关系主题不得跳过 D9。"
),
})
rows.append({
'technique': 'Strength Layers',
'status': 'used' if isinstance(shadbala, dict) and isinstance(ashtakavarga, dict) and isinstance(vimsopaka, dict) else 'blocked',
'source': 'modules.shadbala + modules.ashtakavarga + modules.vimsopaka',
'note': (
f"Shadbala status={shadbala.get('status') if isinstance(shadbala, dict) else None}; "
f"Ashtakavarga SAV={((ashtakavarga.get('sav') or {}).get('total')) if isinstance(ashtakavarga, dict) else None}; "
f"Vimsopaka status={vimsopaka.get('status') if isinstance(vimsopaka, dict) else None}"
),
})
event_judgement = relationship.get('event_judgement') if isinstance(relationship, dict) else {}
secondary_context = event_judgement.get('secondary_context') if isinstance(event_judgement, dict) else []
synastry_context = [item for item in (secondary_context or []) if isinstance(item, str) and item.startswith('synastry_')]
rows.append({
'technique': 'Relationship Synastry Taxonomy',
'status': 'used' if synastry_context else 'blocked',
'source': 'relationship strict workflow + synastry_relationship_bridge_v1',
'note': (
f"relationship secondary_context 中的 synastry 语义={synastry_context}; "
"compatibility support 表示匹配/延续性支持;"
"protective kuta support 表示防护型 Kuta 清洁度支持;"
"若 dual dasha / external timing / marriage convergence 冲突,"
"不得把这些支持越权解释成 legal marriage 的高置信度落地。"
),
})
vedastro_meta = vedastro_overview.get('source_metadata') if isinstance(vedastro_overview, dict) else {}
rows.append({
'technique': 'VedAstro Main Entry Overview',
'status': 'used' if isinstance(vedastro_overview, dict) and vedastro_overview.get('status') == 'ok' else 'blocked',
'source': 'modules.vedastro_range_scan_result',
'note': (
f"overview only; status={vedastro_overview.get('status') if isinstance(vedastro_overview, dict) else None}; "
f"domain_statuses={vedastro_meta.get('domain_statuses') if isinstance(vedastro_meta, dict) else None}; "
f"reference_date={vedastro_meta.get('reference_date') if isinstance(vedastro_meta, dict) else None}."
),
})
return rows
def _build_relationship_narrative_payload(relationship_strict):
if not isinstance(relationship_strict, dict) or not relationship_strict:
return {
'headline': '婚恋严格裁决证据尚未完成,当前不能生成高严谨关系叙事。',
'strengths': [],
'risks': ['缺少 relationship strict workflow 的核心证据,婚恋正文需降级。'],
'boundaries': [
'未完成 D1 + D9 + UL + dual dasha 交叉前,不得把单一关系信号写成高置信度婚姻结论。',
],
'monthly_frame': {
'primary_state': {'value': 'blocked'},
'manifestation_mode': {'value': 'blocked'},
'friction_source': {'value': 'blocked'},
'time_confidence': {'value': 'blocked'},
},
'markdown': (
"### 婚恋严格裁决\n"
"- 当前缺少 relationship strict evidence,无法生成高严谨婚恋 narrative。\n"
"- 在 D1、D9、UL、Vimshottari 与 Narayana 未齐备前,应标记为 blocked 或降低置信度。"
),
}
event_judgement = relationship_strict.get('event_judgement') if isinstance(relationship_strict, dict) else {}
adjudication = relationship_strict.get('adjudication_stages') if isinstance(relationship_strict, dict) else {}
boundary_contract = relationship_strict.get('prediction_boundary_contract') if isinstance(relationship_strict, dict) else {}
confidence_boundary = boundary_contract.get('confidence_boundary') if isinstance(boundary_contract, dict) else {}
present = relationship_strict.get('present_evidence') if isinstance(relationship_strict, dict) else {}
missing = relationship_strict.get('missing_evidence') or []
secondary_context = event_judgement.get('secondary_context') if isinstance(event_judgement, dict) else []
secondary_context = secondary_context if isinstance(secondary_context, list) else []
confidence_cap = relationship_strict.get('confidence_cap') or event_judgement.get('confidence_cap') or 'unknown'
dominant_label = event_judgement.get('dominant_label') if isinstance(event_judgement, dict) else None
monthly_frame = relationship_strict.get('monthly_adjudication_summary') if isinstance(relationship_strict, dict) else {}
monthly_frame = monthly_frame if isinstance(monthly_frame, dict) else {}
synastry = present.get('synastry_relationship_support') if isinstance(present, dict) else {}
synastry_signals = synastry.get('signals') if isinstance(synastry, dict) else []
synastry_signals = synastry_signals if isinstance(synastry_signals, list) else []
strengths = []
risks = []
boundaries = []
if dominant_label == 'legal_marriage':
strengths.append('本轮严格裁决已把婚恋主标签抬到 legal_marriage,但仍需尊重时机与现实承诺层。')
elif dominant_label == 'public_formalization':
strengths.append('当前更偏向 public_formalization,表示关系可见度/公开化支持强于法律婚姻落地。')
elif 'public_formalization_candidate' in secondary_context:
strengths.append('当前更接近 public_formalization_candidate,表示公开化/关系可见度候选正在增强,但仍未达到法律婚姻落地。')
if 'jaimini_support' in secondary_context:
strengths.append('Jaimini 桥接已提供配偶征象支持,DK/UL 线索可用于补强婚恋叙事。')
if 'ul_support' in secondary_context:
strengths.append('Upapada Lagna 已进入严格证据,可作为关系承诺与婚姻叙事的辅助锚点。')
if 'synastry_support' in secondary_context:
strengths.append('合盘支持已进入婚恋主链,但它只说明关系兼容度有帮助,不能单独决定婚姻落地。')
if 'synastry_compatibility_support' in secondary_context:
strengths.append('protective kuta / compatibility support 说明部分 Kuta 与关系延续性维度较干净。')
if 'synastry_protective_kuta_support' in secondary_context:
strengths.append('protective kuta support 已被识别,可作为关系稳定性的次级支持语义。')
if 'synastry_exception_mitigated' in secondary_context:
strengths.append('存在 exception mitigation,说明部分 Dosha/不利匹配在传统规则里有缓解条件。')
if monthly_frame.get('primary_state', {}).get('value'):
strengths.append(f"月度主状态:{monthly_frame.get('primary_state', {}).get('value')}")
if monthly_frame.get('manifestation_mode', {}).get('value'):
strengths.append(f"落地形式:{monthly_frame.get('manifestation_mode', {}).get('value')}")
if confidence_cap in {'low', 'blocked'}:
risks.append('当前 confidence cap 偏低,dual dasha / external timing / marriage convergence 至少有一层存在冲突或不足。')
if 'public_formalization_candidate' in secondary_context:
risks.append('当前虽更接近 public_formalization_candidate,但在 timing conflict 未解除前,不能误读成接近结婚。')
if missing:
risks.append(f"仍缺少关键层:{', '.join(str(item) for item in missing[:4])}")
if 'virodhargala_obstruction' in secondary_context:
risks.append('第七宫 Argala 出现阻滞,关系推进可能伴随现实阻力或时间延后。')
if 'dignity_high_friction' in secondary_context:
risks.append('相关婚恋行星尊贵度摩擦较高,关系推进时更容易出现磨损与反复确认。')
if 'shadbala_component_gap' in secondary_context:
risks.append('Shadbala 六分量还存在缺口,关系强弱结论需继续保守处理。')
if monthly_frame.get('friction_source', {}).get('value'):
risks.append(f"阻力来源:{monthly_frame.get('friction_source', {}).get('value')}")
boundaries.append('婚恋高严谨模式至少需要 D1、D9、UL、Vimshottari 与 Narayana dual dasha 同时在场。')
boundaries.append('protective kuta support、Mahendra、Stree Deergha 等合盘细信号只能辅助,不得越权抬升 legal_marriage。')
boundaries.append('若 dual dasha、external timing 或 marriage convergence 冲突,必须明确降置信度,而不是把关系窗口包装成婚姻必然落地。')
if monthly_frame.get('time_confidence', {}).get('value'):
boundaries.append(f"时间置信度:{monthly_frame.get('time_confidence', {}).get('value')}")
if 'public_formalization_candidate' in secondary_context:
boundaries.append('public_formalization_candidate 只表示公开化候选,不等于法律婚姻,不能越权替代 legal_marriage。')
if synastry_signals:
boundaries.append(f"当前 synastry taxonomy 已命中 {', '.join(synastry_signals[:5])},但这些信号仍从属于 secondary-context。")
if not strengths:
strengths.append('当前婚恋 strict workflow 主要提供边界与缺口提示,尚未形成足够稳定的正向落地支持。')
if not risks:
risks.append('未见强烈负面冲突,但仍需用现实事件、D9 和 dual dasha 做最后复核。')
headline = (
'婚恋严格裁决已接入 synastry taxonomy,可把合盘支持翻译成次级关系语义。'
if 'synastry_support' in secondary_context
else '婚恋严格裁决已接入主链,但当前更依赖本命、D9 与时机层,而非合盘辅助。'
)
markdown_lines = [
'### 婚恋严格裁决',
f"- headline: {headline}",
f"- promise: {(adjudication.get('promise') or {}).get('status') or 'missing'} / drivers={(adjudication.get('promise') or {}).get('drivers') or []}",
f"- activation: {(adjudication.get('activation') or {}).get('status') or 'missing'} / drivers={(adjudication.get('activation') or {}).get('drivers') or []}",
f"- manifestation: {(adjudication.get('manifestation') or {}).get('status') or 'missing'} / drivers={(adjudication.get('manifestation') or {}).get('drivers') or []}",
f"- label: {(adjudication.get('label') or {}).get('value') or dominant_label or 'none'}",
f"- confidence_boundary: MEVG={confidence_boundary.get('mevg_status') or 'blocked'}; Real Case Calibration={confidence_boundary.get('real_case_calibration_status') or 'blocked'}; policy={confidence_boundary.get('unverified_claim_policy') or 'downgrade_or_block'}",
f"- dominant_label: {dominant_label or 'none'}",
f"- confidence_cap: {confidence_cap}",
f"- secondary_context: {secondary_context}",
f"- 月度主状态: {monthly_frame.get('primary_state', {}).get('value') or 'blocked'}",
f"- 落地形式: {monthly_frame.get('manifestation_mode', {}).get('value') or 'blocked'}",
f"- 阻力来源: {monthly_frame.get('friction_source', {}).get('value') or 'blocked'}",
f"- 时间置信度: {monthly_frame.get('time_confidence', {}).get('value') or 'blocked'}",
'- strengths:',
*[f" - {item}" for item in strengths],
'- risks:',
*[f" - {item}" for item in risks],
'- boundaries:',
*[f" - {item}" for item in boundaries],
]
return {
'headline': headline,
'strengths': strengths,
'risks': risks,
'boundaries': boundaries,
'monthly_frame': {
'primary_state': monthly_frame.get('primary_state') or {'value': 'blocked'},
'manifestation_mode': monthly_frame.get('manifestation_mode') or {'value': 'blocked'},
'friction_source': monthly_frame.get('friction_source') or {'value': 'blocked'},
'time_confidence': monthly_frame.get('time_confidence') or {'value': 'blocked'},
},
'markdown': "\n".join(markdown_lines),
'output_template_status': 'used',
'required_sections': ['promise', 'activation', 'manifestation', 'label', 'confidence_boundary'],
}
def _base_strict_narrative_payload(route_label, strict, *, fallback_headline, strengths, risks, boundaries):
monthly_frame = strict.get('monthly_adjudication_summary') if isinstance(strict, dict) else {}
monthly_frame = monthly_frame if isinstance(monthly_frame, dict) else {}
event_judgement = strict.get('event_judgement') if isinstance(strict, dict) else {}
event_judgement = event_judgement if isinstance(event_judgement, dict) else {}
adjudication = strict.get('adjudication_stages') if isinstance(strict, dict) else {}
adjudication = adjudication if isinstance(adjudication, dict) else {}
boundary_contract = strict.get('prediction_boundary_contract') if isinstance(strict, dict) else {}
boundary_contract = boundary_contract if isinstance(boundary_contract, dict) else {}
confidence_boundary = boundary_contract.get('confidence_boundary') if isinstance(boundary_contract, dict) else {}
confidence_boundary = confidence_boundary if isinstance(confidence_boundary, dict) else {}
confidence_cap = strict.get('confidence_cap') or event_judgement.get('confidence_cap') or 'unknown'
dominant_label = event_judgement.get('dominant_label') if isinstance(event_judgement, dict) else None
if monthly_frame.get('primary_state', {}).get('value'):
strengths = list(strengths) + [f"月度主状态:{monthly_frame.get('primary_state', {}).get('value')}"]
if monthly_frame.get('manifestation_mode', {}).get('value'):
strengths = list(strengths) + [f"落地形式:{monthly_frame.get('manifestation_mode', {}).get('value')}"]
if monthly_frame.get('friction_source', {}).get('value'):
risks = list(risks) + [f"阻力来源:{monthly_frame.get('friction_source', {}).get('value')}"]
if monthly_frame.get('time_confidence', {}).get('value'):
boundaries = list(boundaries) + [f"时间置信度:{monthly_frame.get('time_confidence', {}).get('value')}"]
markdown_lines = [
f"### {route_label}严格裁决",
f"- headline: {fallback_headline}",
f"- promise: {(adjudication.get('promise') or {}).get('status') or 'missing'} / drivers={(adjudication.get('promise') or {}).get('drivers') or []}",
f"- activation: {(adjudication.get('activation') or {}).get('status') or 'missing'} / drivers={(adjudication.get('activation') or {}).get('drivers') or []}",
f"- manifestation: {(adjudication.get('manifestation') or {}).get('status') or 'missing'} / drivers={(adjudication.get('manifestation') or {}).get('drivers') or []}",
f"- label: {(adjudication.get('label') or {}).get('value') or dominant_label or 'none'}",
f"- confidence_boundary: MEVG={confidence_boundary.get('mevg_status') or 'blocked'}; Real Case Calibration={confidence_boundary.get('real_case_calibration_status') or 'blocked'}; policy={confidence_boundary.get('unverified_claim_policy') or 'downgrade_or_block'}",
f"- dominant_label: {dominant_label or 'none'}",
f"- confidence_cap: {confidence_cap}",
f"- 月度主状态: {monthly_frame.get('primary_state', {}).get('value') or 'blocked'}",
f"- 落地形式: {monthly_frame.get('manifestation_mode', {}).get('value') or 'blocked'}",
f"- 阻力来源: {monthly_frame.get('friction_source', {}).get('value') or 'blocked'}",
f"- 时间置信度: {monthly_frame.get('time_confidence', {}).get('value') or 'blocked'}",
'- strengths:',
*[f" - {item}" for item in strengths],
'- risks:',
*[f" - {item}" for item in risks],
'- boundaries:',
*[f" - {item}" for item in boundaries],
]
return {
'headline': fallback_headline,
'strengths': list(strengths),
'risks': list(risks),
'boundaries': list(boundaries),
'monthly_frame': {
'primary_state': monthly_frame.get('primary_state') or {'value': 'blocked'},
'manifestation_mode': monthly_frame.get('manifestation_mode') or {'value': 'blocked'},
'friction_source': monthly_frame.get('friction_source') or {'value': 'blocked'},
'time_confidence': monthly_frame.get('time_confidence') or {'value': 'blocked'},
},
'markdown': "\n".join(markdown_lines),
'output_template_status': 'used',
'required_sections': ['promise', 'activation', 'manifestation', 'label', 'confidence_boundary'],
}
def _build_career_narrative_payload(career_strict):
if not isinstance(career_strict, dict) or not career_strict:
return _base_strict_narrative_payload(
'事业',
{},
fallback_headline='事业严格裁决证据尚未完成,当前不能生成高严谨事业叙事。',
strengths=[],
risks=['缺少 career strict workflow 的核心证据,事业正文需降级。'],
boundaries=['未完成 D1、D10、A10、Vimshottari 与 Narayana 交叉前,不得把单一事业信号写成高置信度结论。'],
)
event_judgement = career_strict.get('event_judgement') if isinstance(career_strict, dict) else {}
secondary_context = event_judgement.get('secondary_context') if isinstance(event_judgement, dict) else []
secondary_context = secondary_context if isinstance(secondary_context, list) else []
missing = career_strict.get('missing_evidence') or []
strengths = []
risks = []
boundaries = [
'事业高严谨模式至少需要 D1、D10、A10、Functional Benefic/Malefic、Vimshottari 与 Narayana dual dasha 同时在场。',
'VedAstro 官方事件日可以给时间支撑,但不得越权改写本命 promise 与 strict workflow 的边界。',
]
if event_judgement.get('dominant_label') == 'career_status':
strengths.append('事业 strict workflow 已形成主裁决标签,说明职业主题不是泛泛活跃,而是进入可判读窗口。')
if 'a10_active' in secondary_context:
strengths.append('A10/Karma Pada 已进入主链,说明事业结果会更偏向社会角色、职责承接或可见产出。')
if 'amk_active' in secondary_context:
strengths.append('Amatyakaraka 已进入主链,说明职业能力、上级关系或专业角色承担被明显放大。')
if 'karakamsha_context' in secondary_context:
strengths.append('Karakamsha 已提供职业志向语义,适合用来判断方向感而不只是短期机会。')
if missing:
risks.append(f"仍缺少关键层:{', '.join(str(item) for item in missing[:4])}")
if 'virodhargala_obstruction' in secondary_context:
risks.append('事业主轴存在 Argala 阻滞,推进通常伴随现实牵制、流程卡顿或资源不顺。')
if 'dignity_high_friction' in secondary_context:
risks.append('相关事业行星尊贵度摩擦较高,机会不一定消失,但落地成本会明显上升。')
if 'shadbala_component_gap' in secondary_context:
risks.append('Shadbala 六分量仍有缺口,强弱结论需继续保守。')
headline = '事业严格裁决已接入主链,当前结论将强制引用本命 promise、双重大运、官方时间窗与结构阻力。'
return _base_strict_narrative_payload(
'事业',
career_strict,
fallback_headline=headline,
strengths=strengths,
risks=risks,
boundaries=boundaries,
)
def _build_finance_narrative_payload(finance_strict):
if not isinstance(finance_strict, dict) or not finance_strict:
return _base_strict_narrative_payload(
'财富',
{},
fallback_headline='财富严格裁决证据尚未完成,当前不能生成高严谨财富叙事。',
strengths=[],
risks=['缺少 finance strict workflow 的核心证据,财富正文需降级。'],
boundaries=['未完成 D2/D11、财富 promise、Vimshottari 与 Narayana 交叉前,不得把单一财富信号写成高置信度结论。'],
)
event_judgement = finance_strict.get('event_judgement') if isinstance(finance_strict, dict) else {}
secondary_context = event_judgement.get('secondary_context') if isinstance(event_judgement, dict) else []
secondary_context = secondary_context if isinstance(secondary_context, list) else []
missing = finance_strict.get('missing_evidence') or []
strengths = []
risks = []
boundaries = [
'财富高严谨模式至少需要 D2/D11 或等价财富 promise 层、Functional Benefic/Malefic、Vimshottari 与 Narayana dual dasha 同时在场。',
'官方财富日窗口只能帮助判断回款/交易/现金流节奏,不能单独替代本命财富 promise。',
]
if event_judgement.get('dominant_label') == 'income_growth':
strengths.append('财富 strict workflow 已判到 income_growth,说明更偏向真实入账增长,而不是空泛的财运变好。')
if event_judgement.get('dominant_label') == 'public_wealth_status':
strengths.append('财富 strict workflow 已判到 public_wealth_status,说明更像项目回款、公开收入状态或外部可见的收益变化。')
if 'ashtakavarga_wealth_support' in secondary_context:
strengths.append('Ashtakavarga 财富桥接已进入主链,可作为兑现能力的次级支持。')
if missing:
risks.append(f"仍缺少关键层:{', '.join(str(item) for item in missing[:4])}")
if 'avayogi_active' in secondary_context:
risks.append('Avayogi 风险已触发,说明某些看似有钱流动的窗口也可能伴随高代价或错误决策。')
if 'sodhita_wealth_friction' in secondary_context or 'ashtakavarga_wealth_friction' in secondary_context:
risks.append('财富桥接层已提示兑现摩擦,现金流并不等于可自由留存。')
if 'shadbala_component_gap' in secondary_context:
risks.append('Shadbala 六分量仍有缺口,财富强弱结论需继续保守。')
headline = '财富严格裁决已接入主链,当前结论会强制区分收入兑现、现金流动作与风险摩擦。'
return _base_strict_narrative_payload(
'财富',
finance_strict,
fallback_headline=headline,
strengths=strengths,
risks=risks,
boundaries=boundaries,
)
def _build_vedastro_overview_payload(modules):
overview = modules.get('vedastro_range_scan_result') if isinstance(modules, dict) else {}
if not isinstance(overview, dict):
return {
'status': 'blocked',
'source': 'vedastro_service_adapter_candidate',
'ingestion_profile': None,
'search_scope': None,
'reference_date': None,
'event_count': 0,
'domain_statuses': {},
'top_events_by_domain': {},
'boundary_note': 'VedAstro main-entry overview was not attached.',
'visibility': 'user_visible_overview_only',
}
metadata = overview.get('source_metadata') if isinstance(overview.get('source_metadata'), dict) else {}
return {
'status': overview.get('status') or 'blocked',
'source': overview.get('backend') or 'vedastro_service_adapter_candidate',
'ingestion_profile': metadata.get('ingestion_profile'),
'search_scope': metadata.get('search_scope'),
'reference_date': metadata.get('reference_date'),
'event_count': int(overview.get('event_count', 0) or 0),
'domain_statuses': metadata.get('domain_statuses') or {},
'top_events_by_domain': overview.get('top_events_by_domain') or {},
'boundary_note': (
overview.get('reason')
or 'This is overview only and does not replace explicit long-range VedAstro scans.'
),
'visibility': 'user_visible_overview_only',
}
def _full_reading_profiler_enabled(args) -> bool:
if bool(getattr(args, 'profile_stages', False)):
return True
env_value = os.environ.get('JYOTISH_PROFILE_STAGES', '').strip().lower()
return env_value in {'1', 'true', 'yes', 'on'}
def _record_stage_timing(stage_timings, stage, started_at, *, enabled=False, status='ok', details=None):
elapsed = round(time.perf_counter() - started_at, 4)
entry = {
'stage': stage,
'elapsed_seconds': elapsed,
'status': status,
}
if details:
entry['details'] = details
stage_timings.append(entry)
if enabled:
print(f"[full-reading stage] {stage}: {elapsed:.4f}s ({status})", file=sys.stderr)
return entry
def _build_unified_stage_contract(stage_timings):
groups = {
'local_core': [
'core_chart_and_setup',
'dasha_and_core_varga_stack',
'advanced_interpretation_and_timing_layers',
'dynamic_hooks',
],
'official_evidence': [
'vedastro_official_snapshot',
'vedastro_main_entry_overview',
],
'contract_and_prompt': [
'strict_contracts',
'guided_topics',
'ai_prompt_pack',
],
}
grouped_rows = []
for group_name, stage_names in groups.items():
matched = [row for row in stage_timings if row.get('stage') in stage_names]
grouped_rows.append({
'group': group_name,
'stages': [row.get('stage') for row in matched],
'elapsed_seconds': round(
sum(float(row.get('elapsed_seconds', 0) or 0) for row in matched),
4,
),
'execution_mode': (
'sync_remote_heavy' if group_name == 'official_evidence'
else 'sync_structuring' if group_name == 'contract_and_prompt'
else 'sync_local'
),
})
return {
'stage_contract_version': 1,
'stage_groups': grouped_rows,
'cache_recommendations': {
'api_chart_response': 'recommended',
'official_full_snapshot_semantic': 'recommended',
},
'async_recommendations': {
'chart_async_optional': True,
'high_rigor_async_recommended': True,
},
}
STRICT_WORKFLOW_MODULE_MAP = {
'relationship': 'relationship_strict_evidence',
'career': 'career_strict_evidence',
'finance': 'finance_strict_evidence',
}
def _compact_strict_workflow_contract(strict):
if not isinstance(strict, dict) or not strict:
return None
return {
'question_type': strict.get('question_type'),
'confidence_cap': strict.get('confidence_cap'),
'blocked': bool(strict.get('blocked')),
'reason': strict.get('reason'),
'required_evidence': strict.get('required_evidence') or [],
'missing_evidence': strict.get('missing_evidence') or [],
'official_primary_evidence': strict.get('official_primary_evidence') or {},
'local_supplemental_evidence': strict.get('local_supplemental_evidence') or {},
'fallback_used': strict.get('fallback_used') or [],
'blocked_items': strict.get('blocked_items') or [],
'conflicts': strict.get('conflicts') or [],
'technique_audit_summary': strict.get('technique_audit_summary') or {},
'adjudication_stages': strict.get('adjudication_stages') or {},
'prediction_boundary_contract': strict.get('prediction_boundary_contract') or {},
'domain_invocation_contract': strict.get('domain_invocation_contract') or {},
'output_template_contract': strict.get('output_template_contract') or {},
'mevg_collection_queue': strict.get('mevg_collection_queue') or {},
'real_case_calibration_layer': strict.get('real_case_calibration_layer') or {},
'technical_debt_contract': strict.get('technical_debt_contract') or {},
'remaining_priority1_batch_queue': strict.get('remaining_priority1_batch_queue') or {},
'oracle_parity_queue': strict.get('oracle_parity_queue') or {},
'release_hygiene_plan': strict.get('release_hygiene_plan') or {},
'multi_reference_reading_summary': strict.get('multi_reference_reading_summary') or {},
'monthly_adjudication_summary': strict.get('monthly_adjudication_summary') or {},
'official_day_signal_summary': strict.get('official_day_signal_summary') or {},
'strict_adjudication_bundle': strict.get('strict_adjudication_bundle') or {},
'verdict': strict.get('verdict'),
'dominant_label': strict.get('dominant_label'),
'main_conflicts': strict.get('main_conflicts') or [],
}
def _extract_strict_workflow_contracts(modules):
contracts = {}
if not isinstance(modules, dict):
return contracts
for route, module_name in STRICT_WORKFLOW_MODULE_MAP.items():
contract = _compact_strict_workflow_contract(modules.get(module_name))
if contract:
contracts[route] = contract
return contracts
def _preferred_strict_workflow_contract(contracts):
if not isinstance(contracts, dict):
return None, {}
for route in ('relationship', 'career', 'finance'):
contract = contracts.get(route)
if isinstance(contract, dict) and contract:
return route, contract
return None, {}
def _build_vedastro_official_full_snapshot_payload(modules):
snapshot = modules.get('vedastro_official_full_snapshot') if isinstance(modules, dict) else {}
strict_workflow_contracts = _extract_strict_workflow_contracts(modules)
primary_route, primary_contract = _preferred_strict_workflow_contract(strict_workflow_contracts)
official_primary_evidence = primary_contract.get('official_primary_evidence') if isinstance(primary_contract, dict) else {}
local_supplemental_evidence = primary_contract.get('local_supplemental_evidence') if isinstance(primary_contract, dict) else {}
fallback_used = primary_contract.get('fallback_used') if isinstance(primary_contract, dict) else []
blocked_items = primary_contract.get('blocked_items') if isinstance(primary_contract, dict) else []
conflicts = primary_contract.get('conflicts') if isinstance(primary_contract, dict) else []
if not isinstance(snapshot, dict) or not snapshot:
return {
'status': 'blocked',
'available': False,
'operation': 'official_full_snapshot',
'primary_source': 'vedastro_official',
'strict_workflow_primary_route': primary_route,
'strict_workflow_routes_available': list(strict_workflow_contracts.keys()),
'strict_workflow_contracts': strict_workflow_contracts,
'official_primary_evidence': official_primary_evidence or {},
'local_supplemental_evidence': local_supplemental_evidence or {},
'fallback_used': fallback_used or [],
'blocked_items': blocked_items or [],
'conflicts': conflicts or [],
'boundary_note': 'VedAstro official full snapshot is not attached.',
}
manifest = snapshot.get('request_manifest') if isinstance(snapshot.get('request_manifest'), dict) else {}
requests = manifest.get('requests') if isinstance(manifest.get('requests'), list) else []
snapshot_sections = snapshot.get('snapshot_sections') if isinstance(snapshot.get('snapshot_sections'), dict) else {}
metadata = snapshot.get('source_metadata') if isinstance(snapshot.get('source_metadata'), dict) else {}
full_catalog = metadata.get('official_full_capability_catalog') if isinstance(metadata.get('official_full_capability_catalog'), dict) else {}
dynamic_selection = full_catalog.get('dynamic_selection') if isinstance(full_catalog.get('dynamic_selection'), dict) else {}
report_references = {
theme: selection.get('report_reference')
for theme, selection in dynamic_selection.items()
if isinstance(selection, dict) and isinstance(selection.get('report_reference'), dict)
}
return {
'status': snapshot.get('status') or 'blocked',
'available': bool(snapshot.get('available')),
'operation': snapshot.get('operation') or 'official_full_snapshot',
'primary_source': snapshot.get('primary_source') or 'vedastro_official',
'section_statuses': snapshot.get('section_statuses') or {},
'snapshot_section_keys': sorted(snapshot_sections.keys()),
'request_section_count': len(requests),
'request_sections': [item.get('section') for item in requests if isinstance(item, dict)],
'method_catalog': manifest.get('method_catalog') or {},
'official_full_capability_catalog_status': full_catalog.get('status'),
'official_full_capability_catalog_summary': full_catalog.get('summary') or {},
'official_full_capability_catalog_coverage': full_catalog.get('coverage') or {},
'official_full_capability_domain_routing': full_catalog.get('domain_routing') or {},
'official_full_capability_dynamic_selection': dynamic_selection,
'official_report_references': report_references,
'user_visibility': snapshot.get('user_visibility') or 'backend_raw_evidence_not_direct_user_report',
'source_metadata': snapshot.get('source_metadata') or {},
'strict_workflow_primary_route': primary_route,
'strict_workflow_routes_available': list(strict_workflow_contracts.keys()),
'strict_workflow_contracts': strict_workflow_contracts,
'official_primary_evidence': official_primary_evidence or {},
'local_supplemental_evidence': local_supplemental_evidence or {},
'fallback_used': fallback_used or [],
'blocked_items': blocked_items or [],
'conflicts': conflicts or [],
'boundary_note': (
snapshot.get('reason')
or 'VedAstro official full snapshot is the primary raw evidence layer; user reports consume selected slices only.'
),
}
def _build_ai_prompt_pack(report):
"""Build a compact, evidence-first prompt pack for downstream AI/RAG reading."""
modules = report.get('modules', {}) if isinstance(report, dict) else {}
chart = report.get('chart') or modules.get('chart') or {}
planets = chart.get('planets', {}) if isinstance(chart, dict) else {}
birth_info = chart.get('birth_info', {}) if isinstance(chart, dict) else {}
dasha = modules.get('dasha') or {}
current_dasha = dasha.get('current_dasha') if isinstance(dasha, dict) else {}
narayana = modules.get('narayana_dasha') or {}
dasa_convergence = modules.get('dasa_convergence') or {}
shadbala = modules.get('shadbala') or {}
shadbala_planets = shadbala.get('planets', {}) if isinstance(shadbala, dict) else {}
ashtakavarga = modules.get('ashtakavarga') or {}
sav = ashtakavarga.get('sav') if isinstance(ashtakavarga, dict) else {}
d9 = modules.get('d9_navamsa_expanded') or {}
functional_layer = _functional_benefic_malefic_snapshot(planets, chart.get('ascendant', {}))
oracle_progress = _oracle_progress_snapshot()
technique_audit_table = _build_technique_audit_table(functional_layer, oracle_progress, modules)
relationship_narrative = _build_relationship_narrative_payload(modules.get('relationship_strict_evidence'))
career_narrative = _build_career_narrative_payload(modules.get('career_strict_evidence'))
finance_narrative = _build_finance_narrative_payload(modules.get('finance_strict_evidence'))
vimsopaka_semantic_summary = _build_vimsopaka_semantic_summary(modules.get('vimsopaka'))
vedastro_overview = _build_vedastro_overview_payload(modules)
vedastro_official_full_snapshot = _build_vedastro_official_full_snapshot_payload(modules)
strict_workflow_contracts = _extract_strict_workflow_contracts(modules)
strict_workflow_primary_route, primary_strict_contract = _preferred_strict_workflow_contract(strict_workflow_contracts)
primary_prediction_boundary_contract = (
primary_strict_contract.get('prediction_boundary_contract')
if isinstance(primary_strict_contract, dict)
else {}
)
primary_domain_invocation_layers = (
primary_strict_contract.get('domain_invocation_contract')
if isinstance(primary_strict_contract, dict)
else {}
)
primary_output_template_contract = (
primary_strict_contract.get('output_template_contract')
if isinstance(primary_strict_contract, dict)
else {}
)
primary_mevg_collection_queue = (
primary_strict_contract.get('mevg_collection_queue')
if isinstance(primary_strict_contract, dict)
else {}
)
primary_real_case_calibration_layer = (
primary_strict_contract.get('real_case_calibration_layer')
if isinstance(primary_strict_contract, dict)
else {}
)
primary_technical_debt_contract = (
primary_strict_contract.get('technical_debt_contract')
if isinstance(primary_strict_contract, dict)
else {}
)
primary_remaining_priority1_batch_queue = (
primary_strict_contract.get('remaining_priority1_batch_queue')
if isinstance(primary_strict_contract, dict)
else {}
)
primary_oracle_parity_queue = (
primary_strict_contract.get('oracle_parity_queue')
if isinstance(primary_strict_contract, dict)
else {}
)
primary_release_hygiene_plan = (
primary_strict_contract.get('release_hygiene_plan')
if isinstance(primary_strict_contract, dict)
else {}
)
primary_audit = (
primary_strict_contract.get('technique_audit_summary')
if isinstance(primary_strict_contract, dict)
else {}
)
try:
from strict_evidence_service import existing_interpretation_source_pack
fallback_source_pack = existing_interpretation_source_pack()
except Exception:
fallback_source_pack = {}
interpretation_source_audit = (
primary_audit.get('interpretation_source_pack')
if isinstance(primary_audit, dict) and isinstance(primary_audit.get('interpretation_source_pack'), dict)
else {}
)
fallback_domain_layers = (
fallback_source_pack.get('domain_invocation_layers')
if isinstance(fallback_source_pack, dict) and isinstance(fallback_source_pack.get('domain_invocation_layers'), dict)
else {}
)
guided_topics = modules.get('guided_topics') if isinstance(modules.get('guided_topics'), list) else build_guided_topics(report)
capability_evidence_pool = build_capability_evidence_pool_summary()
shadbala_ranking = []
for planet_name, pdata in sorted(
shadbala_planets.items(),
key=lambda item: item[1].get('rank', 99) if isinstance(item[1], dict) else 99,
):
if isinstance(pdata, dict):
shadbala_ranking.append({
'planet': planet_name,
'rank': pdata.get('rank'),
'total_rupas': pdata.get('total_rupas'),
'min_required': pdata.get('min_required'),
'strength_level': pdata.get('strength_level'),
})
current_ad = current_dasha.get('antardasha') if isinstance(current_dasha, dict) else {}
evidence_snapshot = {
'birth': report.get('birth_info', {}),
'ayanamsa': {
'name': birth_info.get('ayanamsa_name', 'lahiri'),
'display': birth_info.get('ayanamsa_display', 'Lahiri'),
'value': birth_info.get('ayanamsa'),
'node_mode': birth_info.get('node_mode'),
},
'core': {
'ascendant': chart.get('ascendant', {}),
'Sun': _planet_snapshot(planets, 'Sun'),
'Moon': _planet_snapshot(planets, 'Moon'),
'Mars': _planet_snapshot(planets, 'Mars'),
'Jupiter': _planet_snapshot(planets, 'Jupiter'),
'Venus': _planet_snapshot(planets, 'Venus'),
'Saturn': _planet_snapshot(planets, 'Saturn'),
'Rahu': _planet_snapshot(planets, 'Rahu'),
'Ketu': _planet_snapshot(planets, 'Ketu'),
},
'timing': {
'vimshottari': {
'mahadasha': current_dasha.get('lord') if isinstance(current_dasha, dict) else None,
'antardasha': current_ad.get('lord') if isinstance(current_ad, dict) else None,
'start': current_dasha.get('start') if isinstance(current_dasha, dict) else None,
'end': current_dasha.get('end') if isinstance(current_dasha, dict) else None,
},
'narayana': {
'current_dasha': narayana.get('current_dasha') if isinstance(narayana, dict) else None,
'current_year': narayana.get('current_year') if isinstance(narayana, dict) else None,
'current_age': narayana.get('current_age') if isinstance(narayana, dict) else None,
},
'convergence_top_domains': dasa_convergence.get('top_convergent_domains', []) if isinstance(dasa_convergence, dict) else [],
},
'strength': {
'shadbala_ranking': shadbala_ranking[:7],
'sav_total': sav.get('total') if isinstance(sav, dict) else None,
'sav_scores': sav.get('scores') if isinstance(sav, dict) else None,
},
'varga_focus': {
'd9': {
'Ascendant': d9.get('Ascendant') if isinstance(d9, dict) else None,
'Venus': d9.get('Venus') if isinstance(d9, dict) else None,
'Jupiter': d9.get('Jupiter') if isinstance(d9, dict) else None,
'Mars': d9.get('Mars') if isinstance(d9, dict) else None,
'Saturn': d9.get('Saturn') if isinstance(d9, dict) else None,
}
},
'quality_boundary': {
'errors': report.get('errors', []),
'warnings': report.get('warnings', []),
'external_oracle_status': 'D1/D9/VedAstro longitude boundary covered; Dasha/Shadbala external absolute calibration still requires multi-source oracle expansion.',
},
'oracle_progress': oracle_progress,
'functional_benefic_malefic': functional_layer,
'interpretation_source_pack': {
'status': interpretation_source_audit.get('status') or fallback_source_pack.get('status') or 'blocked',
'source': interpretation_source_audit.get('source') or fallback_source_pack.get('source') or 'repo_existing_interpretation_sources',
'core_rule_source_refs': interpretation_source_audit.get('core_rule_source_refs') or fallback_source_pack.get('core_rule_source_layer', {}).get('source_refs') or [],
'promote_batch2_source_refs': interpretation_source_audit.get('promote_batch2_source_refs') or fallback_source_pack.get('promote_batch2_topic_layer', {}).get('source_refs') or [],
'reference_only_source_refs': interpretation_source_audit.get('reference_only_source_refs') or fallback_source_pack.get('reference_only_conflict_layer', {}).get('source_refs') or [],
'missing_refs': interpretation_source_audit.get('missing_refs') or fallback_source_pack.get('missing_refs') or [],
},
'prediction_boundary_contract': primary_prediction_boundary_contract or {},
'domain_invocation_layers': primary_domain_invocation_layers or fallback_domain_layers or {},
'output_template_contract': primary_output_template_contract or {},
'mevg_collection_queue': primary_mevg_collection_queue or {},
'real_case_calibration_layer': primary_real_case_calibration_layer or {},
'technical_debt_contract': primary_technical_debt_contract or {},
'remaining_priority1_batch_queue': primary_remaining_priority1_batch_queue or {},
'oracle_parity_queue': primary_oracle_parity_queue or {},
'release_hygiene_plan': primary_release_hygiene_plan or {},
'strict_workflow_primary_route': strict_workflow_primary_route,
'strict_workflow_routes_available': list(strict_workflow_contracts.keys()),
'strict_workflow_contracts': strict_workflow_contracts,
'official_primary_evidence': primary_strict_contract.get('official_primary_evidence') if isinstance(primary_strict_contract, dict) else {},
'local_supplemental_evidence': primary_strict_contract.get('local_supplemental_evidence') if isinstance(primary_strict_contract, dict) else {},
'fallback_used': primary_strict_contract.get('fallback_used') if isinstance(primary_strict_contract, dict) else [],
'blocked_items': primary_strict_contract.get('blocked_items') if isinstance(primary_strict_contract, dict) else [],
'conflicts': primary_strict_contract.get('conflicts') if isinstance(primary_strict_contract, dict) else [],
'vedastro_official_full_snapshot': vedastro_official_full_snapshot,
'vedastro_overview': vedastro_overview,
'guided_topics': guided_topics,
'capability_evidence_pool': capability_evidence_pool,
'technique_audit_table': technique_audit_table,
'career_narrative': career_narrative,
'relationship_narrative': relationship_narrative,
'finance_narrative': finance_narrative,
'vimsopaka_semantic_summary': vimsopaka_semantic_summary,
}
prompt_lines = [
"你是一个审慎的 AI Native 印度/吠陀占星分析助手。",
"请只基于 evidence_snapshot 中的计算证据生成解读,不要编造星盘不存在的配置。",
f"本盘使用 {evidence_snapshot['ayanamsa']['display']} ayanamsa,节点口径为 {evidence_snapshot['ayanamsa']['node_mode']}",
"必须遵守:不要仅凭单一配置下结论;每个核心判断至少交叉 D1、D9、Dasha、Shadbala/Ashtakavarga 或 Transit 中的两个证据层。",
"必须显式标注置信度和边界:Dasha/PDF 起点差异、Shadbala 外部绝对值 oracle 尚未完成时,不得声称已经完全校准。",
"输出结构建议:参数声明、核心星盘、关系/事业/财富/健康分主题、当前时机、证据表、风险边界、可行动建议。",
"若引用经典法则,请优先检索 retrieval_plan.local_reference_docs;需要外部断语时再做 web/source verification。",
"若 evidence_snapshot.vedastro_overview.status 为 ok,请把它作为用户可见外部概览证据明确写出,但不要把 overview-only 结果误当作长周期精扫结论。",
"VedAstro 官方全量快照是第一原始证据层;若 evidence_snapshot.vedastro_official_full_snapshot.status 不是 ok/partial,必须说明官方全量资料 blocked,并把本地结果标记为 fallback。",
"若 evidence_snapshot.capability_evidence_pool 存在,请把 89 项视为后台备选证据池;不要把所有能力条目平铺成结论,也不要让 audit_only/alias 条目影响占星判断。",
"必须按 promise → activation → manifestation → label 输出;每个判断都要说明属于承诺、激活、落地形式还是标签层。",
"未完成 MEVG / Real Case Calibration 时必须降级或标 blocked,不得把内部一致性写成已验证结论。",
]
return {
'schema_version': 1,
'mode': 'jyotish_structured_prompt_pack',
'prompt_zh': "\n".join(prompt_lines),
'evidence_snapshot': evidence_snapshot,
'retrieval_plan': {
'local_reference_docs': [
'references/ai-reading-workflow-prompt.md',
'references/comprehensive-reading-workflow.md',
'references/prediction-boundary-protocol.md',
'references/event_judgment_skeleton.md',
'references/planetary-dignity-complete-reference.md',
'references/retrograde-combustion-war-guide.md',
'references/transit-multi-reference-guide.md',
'references/vimshottari_dasha_guide.md',
'references/pratyantar-calculation-guide.md',
'references/divisional-chart-deep-reading.md',
'references/shadbala-complete-methodology.md',
'references/ashtakavarga-complete-system.md',
'references/tajika-yoga-complete-guide.md',
'references/jaimini-complete-system.md',
'references/kp-astrology-complete-system.md',
'references/argala-complete-guide.md',
'references/badhaka-obstacle-planet-guide.md',
'references/condition-dasha-complete.md',
'references/dasa-convergence-methodology.md',
'references/multi-dasha-convergence-protocol.md',
'references/yoga-strength-scoring-system.md',
'references/shadbala-interpretation-methodology.md',
'references/navamsa-d9-interpretation-template.md',
'references/interpretation_template_registry.json',
'references/open_source_sources/vedic-astro-skills/codex/skills/vedic-core/resources/p1_p12.md',
'references/open_source_sources/vedic-astro-skills/codex/skills/vedic-core/resources/house_framework.md',
'references/raman-house-judgment-methodology.md',
'references/mandatory-verification-gate-protocol.md',
'references/real-reading-quality-checklist.md',
],
'retrieval_tags': [
'no_single_factor_conclusion',
'd1_d9_dasha_cross_validation',
'oracle_boundary_visible',
'external_oracle_evidence_validation',
'confidence_labeled_reading',
'interpretation_source_pack',
'mevg_global_web_evidence_required',
'real_case_calibration_required',
],
},
}
def _overview_vedastro_reference_date(args) -> str:
candidate = (
getattr(args, 'transit_date', None)
or getattr(args, 'today', None)
or datetime.now().strftime('%Y-%m-%d')
)
return str(candidate)[:10]
def _overview_vedastro_scan_window(reference_date: str) -> tuple[str, str]:
start = datetime.strptime(reference_date, '%Y-%m-%d').date()
return start.isoformat(), start.isoformat()
def _attach_vedastro_main_entry_overview(report, args):
if not isinstance(report, dict):
return report
modules = report.setdefault('modules', {})
if not isinstance(modules, dict):
return report
if modules.get('vedastro_range_scan_result'):
return report
try:
from vedastro_service_adapter import run_range_scan_for_case
except Exception as exc: # pragma: no cover - import guard
report.setdefault('warnings', []).append(f"vedastro-main-entry-import: {exc}")
return report
reference_date = _overview_vedastro_reference_date(args)
start_date, end_date = _overview_vedastro_scan_window(reference_date)
case = {
'year': getattr(args, 'year', None),
'month': getattr(args, 'month', None),
'day': getattr(args, 'day', None),
'hour': getattr(args, 'hour', None),
'minute': getattr(args, 'minute', None),
'second': _arg_second(args),
'lat': getattr(args, 'lat', None),
'lon': getattr(args, 'lon', None),
'tz': getattr(args, 'tz', None),
'ayanamsa_policy': getattr(args, 'ayanamsa', None) or _current_ayanamsa_name(args),
'node_policy': getattr(args, 'node_mode', 'mean'),
'reference_date': (
getattr(args, 'transit_date', None)
or getattr(args, 'today', None)
or datetime.now().strftime('%Y-%m-%d')
),
}
def _scan_domain(domain: str):
return domain, run_range_scan_for_case(
case,
domain=domain,
start_date=start_date,
end_date=end_date,
case_id=f"main_entry_{domain}",
)
domain_reports = {}
combined_events = []
domain_statuses = {}
top_events = {}
daily_windows_by_domain = {}
top_daily_window_by_domain = {}
failure_reason = None
availability = True
with ThreadPoolExecutor(max_workers=3) as executor:
for domain, domain_report in executor.map(_scan_domain, ('career', 'marriage', 'wealth')):
domain_reports[domain] = domain_report
for domain in ('career', 'marriage', 'wealth'):
domain_report = domain_reports[domain]
domain_statuses[domain] = domain_report.get('status')
availability = availability and bool(domain_report.get('available', False))
if domain_report.get('status') != 'ok' and failure_reason is None:
failure_reason = domain_report.get('reason')
for event in domain_report.get('evidence_ledger') or []:
if isinstance(event, dict):
combined_events.append(event)
top_event = domain_report.get('top_event')
if isinstance(top_event, dict):
top_events[domain] = top_event
daily_windows = domain_report.get('daily_windows')
if isinstance(daily_windows, list):
daily_windows_by_domain[domain] = daily_windows
top_daily_window = domain_report.get('top_daily_window')
if isinstance(top_daily_window, dict):
top_daily_window_by_domain[domain] = top_daily_window
primary_status = next(
(
domain_reports[domain].get('status')
for domain in ('career', 'marriage', 'wealth')
if domain_reports.get(domain, {}).get('status') == 'ok'
),
domain_reports.get('marriage', {}).get('status') or 'blocked',
)
source_metadata = {
'ingestion_profile': 'main_entry_overview',
'search_scope': 'single_day_overview',
'reference_date': reference_date,
'scan_window': {'start': start_date, 'end': end_date},
'domain_statuses': domain_statuses,
'domain_event_counts': {
domain: int((domain_reports.get(domain, {}) or {}).get('event_count', 0) or 0)
for domain in ('career', 'marriage', 'wealth')
},
}
for domain in ('career', 'marriage', 'wealth'):
metadata = domain_reports.get(domain, {}).get('source_metadata')
if isinstance(metadata, dict):
for key in (
'endpoint',
'endpoint_host',
'transport',
'provenance_mode',
'timeout_seconds',
'retry_policy',
):
if key in metadata and key not in source_metadata:
source_metadata[key] = metadata[key]
modules['vedastro_range_scan_result'] = {
'backend': 'vedastro_service_adapter_candidate',
'available': availability,
'status': primary_status,
'operation': 'range_scan',
'domain': 'overview',
'event_count': len(combined_events),
'top_event': top_events.get('marriage') or next(iter(top_events.values()), None),
'top_events_by_domain': top_events,
'daily_windows': [
item
for domain in ('career', 'marriage', 'wealth')
for item in (daily_windows_by_domain.get(domain) or [])
if isinstance(item, dict)
],
'top_daily_window': (
top_daily_window_by_domain.get('marriage')
or next(iter(top_daily_window_by_domain.values()), None)
),
'daily_windows_by_domain': daily_windows_by_domain,
'top_daily_window_by_domain': top_daily_window_by_domain,
'evidence_ledger': combined_events,
'source_metadata': source_metadata,
'reason': failure_reason,
'domain_reports': domain_reports,
}
return report
def _attach_vedastro_official_full_snapshot(report, args):
if not isinstance(report, dict):
return report
modules = report.setdefault('modules', {})
if not isinstance(modules, dict):
return report
if modules.get('vedastro_official_full_snapshot'):
return report
try:
from vedastro_service_adapter import run_official_full_snapshot_for_case
from vedastro_priority import apply_vedastro_source_priority
except Exception as exc: # pragma: no cover - import guard
report.setdefault('warnings', []).append(f"vedastro-official-full-snapshot-import: {exc}")
return report
case = {
'year': getattr(args, 'year', None),
'month': getattr(args, 'month', None),
'day': getattr(args, 'day', None),
'hour': getattr(args, 'hour', None),
'minute': getattr(args, 'minute', None),
'second': _arg_second(args),
'lat': getattr(args, 'lat', None),
'lon': getattr(args, 'lon', None),
'tz': getattr(args, 'tz', None),
'ayanamsa_policy': getattr(args, 'ayanamsa', None) or _current_ayanamsa_name(args),
'node_policy': getattr(args, 'node_mode', 'mean'),
'reference_date': (
getattr(args, 'transit_date', None)
or getattr(args, 'today', None)
or datetime.now().strftime('%Y-%m-%d')
),
}
modules['vedastro_official_full_snapshot'] = run_official_full_snapshot_for_case(
case,
case_id='full_reading_official_primary',
)
apply_vedastro_source_priority(
report,
official_snapshot=modules['vedastro_official_full_snapshot'],
)
return report
def _load_strict_evidence_collector():
try:
from strict_evidence_service import collect_strict_evidence as collector
return collector
except Exception:
service_path = os.path.join(SCRIPT_DIR, 'strict_evidence_service.py')
if not os.path.exists(service_path):
raise
spec = importlib.util.spec_from_file_location("jyotish_strict_evidence_service", service_path)
if spec is None or spec.loader is None:
raise ImportError(f"Unable to load strict_evidence_service from {service_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
collector = getattr(module, "collect_strict_evidence", None)
if collector is None:
raise ImportError("strict_evidence_service.collect_strict_evidence not found")
return collector
# ============================================================================
# 公共星盘计算(供 chart/shadbala/ashtakavarga 共用,v3.4提取)
# ============================================================================
def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='mean', second=0, ayanamsa_name=None):
"""计算星盘核心数据,返回 (result_dict, asc_idx, jd, ayanamsa)。node_mode: mean|true。"""
if not HAS_SWE:
return None, None, None, None
swe.set_ephe_path('')
if ayanamsa_name:
_apply_ayanamsa(ayanamsa_name)
ayanamsa_name = _current_ayanamsa_name(type('Args', (), {'ayanamsa': ayanamsa_name})())
second = int(second or 0)
hour_decimal = _birth_hour_decimal(hour, minute, second) - tz
jd = swe.julday(year, month, day, hour_decimal)
ayanamsa = swe.get_ayanamsa(jd)
node_mode = (node_mode or 'mean').lower()
if node_mode not in ('mean', 'true'):
node_mode = 'mean'
node_pid = _node_pid(node_mode)
result = {"birth_info": {
"date": f"{year}-{month:02d}-{day:02d}", "time": _birth_time_string(hour, minute, second),
"hour": int(hour), "minute": int(minute), "second": second,
"tz": f"UTC{'+' if tz >= 0 else ''}{tz}", "lat": lat, "lon": lon,
"julian_day": round(jd, 6), "ayanamsa": round(ayanamsa, 4),
"ayanamsa_name": ayanamsa_name,
"ayanamsa_display": _ayanamsa_display_name(ayanamsa_name),
"node_mode": node_mode, "node_mode_note": "mean=Mean Node; true=True Node. PyJHora默认true,本skill默认mean。"
}, "ascendant": None, "planets": {}, "houses": {}}
asc_lon, _ = swe.houses(jd, lat, lon, b'A')
asc_deg = (asc_lon[0] - ayanamsa) % 360
asc_idx = int(asc_deg / 30)
asc_sign = SIGNS[asc_idx]
deg_in_sign = asc_deg - asc_idx * 30
result["ascendant"] = {"sign": asc_sign, "sign_cn": SIGNS_CN[asc_sign],
"degree": round(deg_in_sign, 4), "degree_raw": round(asc_deg, 4),
"degree_in_sign": round(deg_in_sign, 4),
"degree_in_sign_raw": asc_deg,
"lon": round(asc_deg, 4),
"lord": SIGN_LORDS[asc_sign]}
for i in range(12):
c = (asc_lon[i] - ayanamsa) % 360
si = int(c / 30)
result["houses"][f"house_{i+1}"] = {"cusp_sign": SIGNS[si],
"cusp_sign_cn": SIGNS_CN[SIGNS[si]], "cusp_degree": round(c, 4),
"lord": SIGN_LORDS[SIGNS[si]]}
nak_span = 360.0 / 27
planets_swe = {**BASE_PLANETS_SWE, 'Rahu': node_pid}
for pname, pid in planets_swe.items():
try:
pos, _ = swe.calc_ut(jd, pid)
lon_p = (pos[0] - ayanamsa) % 360; lat_p = pos[1]; spd = pos[3]
si = int(lon_p / 30); d_in_s = lon_p - si * 30; sign = SIGNS[si]
retro = spd < 0
house = ((si - asc_idx) % 12) + 1
ni = int(lon_p / nak_span); pada = int((lon_p % nak_span) / (nak_span / 4)) + 1
nak_n, nak_l, _ = NAKSHATRA_LIST[ni % 27]
result["planets"][pname] = {
"sign": sign, "sign_cn": SIGNS_CN[sign], "degree": round(lon_p, 4),
"degree_raw": lon_p,
"degree_in_sign": round(d_in_s, 4), "degree_in_sign_raw": d_in_s,
"house": house, "status": "",
"retrograde": retro, "speed": round(spd, 6),
"nakshatra": nak_n, "nakshatra_pada": pada, "nakshatra_lord": nak_l}
if pname == 'Rahu':
klon = (lon_p + 180) % 360; ksi = int(klon / 30); kd = klon - ksi * 30
kni = int(klon / nak_span); kp = int((klon % nak_span) / (nak_span / 4)) + 1
kn, kl, _ = NAKSHATRA_LIST[kni % 27]
result["planets"]["Ketu"] = {
"sign": SIGNS[ksi], "sign_cn": SIGNS_CN[SIGNS[ksi]],
"degree": round(klon, 4), "degree_raw": klon,
"degree_in_sign": round(kd, 4), "degree_in_sign_raw": kd,
"house": ((ksi - asc_idx) % 12) + 1, "status": "",
"retrograde": True, "speed": round(spd, 6),
"nakshatra": kn, "nakshatra_pada": kp, "nakshatra_lord": kl}
except Exception as e:
result["planets"][pname] = {"error": str(e)}
for pn, p_data in result["planets"].items():
if "error" not in p_data and pn != "Lagna":
p_data["status"] = _get_planet_status_label(pn, p_data["sign"], p_data["degree_in_sign"], result["planets"])
return result, asc_idx, jd, ayanamsa
# ============================================================================
# 1. 星盘计算
# ============================================================================
def cmd_chart(args):
result, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if result is None:
return {"error": "swisseph未安装"}
# v3.5: --validate 触发 R1-R10 校验
if getattr(args, 'validate', False):
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
asht_result = calc_ashtakavarga(result.get('planets', {}), asc_idx)
from validate import validate_chart
validation = validate_chart(result, asht_result)
result['validation'] = validation
except Exception as e:
result['validation'] = {"error": str(e), "valid": False}
return result
# ============================================================================
# 2. Dasha计算
# ============================================================================
def cmd_dasha(args):
nak_info = None; progress = 0.5
if args.moon_lon is not None:
ns = 360.0 / 27; idx = int(args.moon_lon / ns); progress = (args.moon_lon % ns) / ns
nak_info = NAKSHATRA_LIST[idx % 27]
elif args.nakshatra:
nl = args.nakshatra.lower().replace(" ", "").replace("-", "")
for n in NAKSHATRA_LIST:
if nl in n[0].lower().replace(" ", "") or n[0].lower().replace(" ", "").startswith(nl[:5]):
nak_info = n; break
if not nak_info: return {"error": f"未找到Nakshatra: {args.nakshatra}"}
if args.pada: progress = (max(1, min(4, args.pada)) - 1) / 4 + 0.125
else:
# v6.0.27: Auto-calculate Moon's Nakshatra from birth datetime
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装,无法自动计算Nakshatra"}
moon = chart.get("planets", {}).get("Moon", {})
moon_lon = moon.get("degree_raw")
if moon_lon is None:
return {"error": "无法计算Moon位置"}
ns = 360.0 / 27; idx = int(moon_lon / ns); progress = (moon_lon % ns) / ns
nak_info = NAKSHATRA_LIST[idx % 27]
nak_name, start_lord, start_years = nak_info
birthdate = args.birthdate or f"{args.year}-{args.month:02d}-{args.day:02d}"
has_birth_clock = all(getattr(args, field, None) is not None for field in ("year", "month", "day", "hour", "minute"))
birth_time = _birth_time_string(args.hour, args.minute, _arg_second(args)) if has_birth_clock else None
birth_dt = _birth_datetime_from_args(args) if has_birth_clock else datetime.strptime(birthdate, "%Y-%m-%d")
elapsed = progress * start_years; remaining = start_years - elapsed
dt = birth_dt - timedelta(days=elapsed * 365.25)
si = DASHA_ORDER.index(start_lord)
timeline = []
for i in range(9):
lord = DASHA_ORDER[(si + i) % 9]; years = DASHA_YEARS[lord]
end_dt = dt + timedelta(days=years * 365.25)
# 第一个 MD 的展示 years 用 balance,实际日期计算用完整年数(数学等价)
display_years = round(remaining, 2) if i == 0 else years
timeline.append({
"lord": lord,
"lord_cn": PLANET_CN[lord],
"start": dt.strftime("%Y-%m-%d"),
"end": end_dt.strftime("%Y-%m-%d"),
"start_datetime": dt.isoformat(timespec="seconds"),
"end_datetime": end_dt.isoformat(timespec="seconds"),
"years": display_years,
"full_years": years,
"is_current": False,
"is_balance": i == 0,
"balance_years": round(remaining, 2) if i == 0 else None,
"elapsed_at_birth": round(elapsed, 2) if i == 0 else None,
})
dt = end_dt
today = datetime.strptime(args.today, "%Y-%m-%d") if args.today else datetime.now()
current = None
for d in timeline:
ds = datetime.strptime(d["start"], "%Y-%m-%d"); de = datetime.strptime(d["end"], "%Y-%m-%d")
total_days = (de - ds).days; li = DASHA_ORDER.index(d["lord"])
# v3.7.2: 为所有 Mahadasha 计算 Antardasha(不仅当前大运)
sub = []; sdt = ds
for j in range(9):
sl = DASHA_ORDER[(li + j) % 9]; sd = total_days * DASHA_YEARS[sl] / 120
se = sdt + timedelta(days=sd)
is_cur = sdt <= today < se
sub.append({"lord": sl, "lord_cn": PLANET_CN[sl], "start": sdt.strftime("%Y-%m-%d"), "end": se.strftime("%Y-%m-%d"), "is_current": is_cur})
sdt = se
d["antardasha_timeline"] = sub
if ds <= today < de:
d["is_current"] = True
# 从 antardasha 列表中提取当前正在运行的 antardasha
current_ad = None
for ad in sub:
if ad.get("is_current"):
current_ad = ad
break
d["mahadasha"] = d.get("lord", "")
d["mahadasha_cn"] = d.get("lord_cn", "")
d["antardasha"] = current_ad or (sub[0] if sub else None)
current = d
result = {"moon_nakshatra": nak_name, "birth_date": birthdate, "reference_date": today.strftime("%Y-%m-%d"), "timeline": timeline, "current_dasha": current}
if birth_time:
result["birth_time"] = birth_time
result["birth_datetime"] = f"{birthdate} {birth_time}"
return result
# ============================================================================
# 3. Yoga识别
# ============================================================================
def cmd_yoga(args):
"""
Yoga 识别 —— v6.0.26 数据驱动引擎。
支持两种输入:
1. 兼容旧接口:--ascendant + --planets "Sun:Aries:1[:10.5],Moon:Cancer:4[:15]"
2. 出生信息直算:--year --month --day --hour --minute --lat --lon --tz [--node-mode]
"""
planets = {}
asc = args.ascendant or "Aries"
yoga_context = None
if args.planets:
for item in args.planets.split(','):
parts = item.strip().split(':')
if len(parts) >= 3:
pdata = {"sign": parts[1].strip(), "house": int(parts[2].strip())}
if len(parts) >= 4 and parts[3].strip() != "":
try:
pdata["degree"] = float(parts[3].strip())
except ValueError:
pass
planets[parts[0].strip()] = pdata
context_json = getattr(args, 'context_json', None)
if context_json:
try:
yoga_context = json.loads(context_json)
except Exception:
yoga_context = None
else:
required_birth_fields = ["year", "month", "day", "hour", "minute", "lat", "lon"]
has_birth_input = all(getattr(args, field, None) is not None for field in required_birth_fields)
if has_birth_input:
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, getattr(args, 'tz', 0), getattr(args, 'node_mode', 'mean'),
second=_arg_second(args),
)
if chart is None:
return {"error": "swisseph未安装"}
asc = chart.get("ascendant", {}).get("sign", asc)
planet_lons_for_varga = {}
for pname, pd in chart.get("planets", {}).items():
if isinstance(pd, dict) and "sign" in pd and "house" in pd:
planets[pname] = {
"sign": pd["sign"],
"house": pd["house"],
"degree": pd.get("degree_in_sign", pd.get("degree")),
}
if pd.get("degree_raw") is not None:
planet_lons_for_varga[pname] = pd.get("degree_raw")
try:
from varga import calc_all_vargas
asc_deg_raw = chart.get("ascendant", {}).get("degree_raw")
if asc_deg_raw is not None and planet_lons_for_varga:
yoga_context = _build_yoga_context_from_vargas(
calc_all_vargas(planet_lons_for_varga, asc_deg_raw, [9, 60]),
planet_lons_for_varga,
)
except Exception:
yoga_context = None
ai = SIGNS.index(asc) if asc in SIGNS else 0
kl = list(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 4, 7, 10]]))
tl = list(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 5, 9]]))
# 调用数据驱动引擎(yoga_engine.py
yogas = detect_yogas(planets, asc, context=yoga_context)
return {
"ascendant": asc,
"planets_analyzed": len(planets),
"kendra_lords": kl,
"trikona_lords": tl,
"yogas_detected": len(yogas),
"yogas": yogas,
"detected_yogas": yogas,
}
# ============================================================================
# 4. 三层验证法事件预测(v3.4增强:优先EventPredictionModel规则引擎)
# ============================================================================
def cmd_predict(args):
# 验前事模式(v3.5新增)
if getattr(args, 'past_verify', False) and args.year:
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
return _past_event_verify(chart, asc_idx, args)
chart = json.loads(args.chart) if args.chart else {}
evt = args.event_type or "all"
# 尝试加载 EventPredictionModel(替代LAM神经网络)
try:
sys.path.insert(0, SCRIPT_DIR)
from event_prediction_model import EventPredictionModel
# 直接传完整chart数据给EventPredictionModelv5.0需要ascendant dict和planets dict
# 同时从 full-reading 输出中提取所有模块数据传入(v5.1修复:之前丢失dasha/congregation等)
modules = chart.get("modules", {})
model = EventPredictionModel(
chart_data={
"ascendant": chart.get("ascendant", {}),
"planets": chart.get("planets", {}),
},
dasha_data=modules.get("dasha"),
congregation_data=modules.get("congregation"),
vivah_saham_data=modules.get("vivah_saham"),
chara_dasha_data=modules.get("jaimini", {}).get("chara_dasha"),
)
raw_preds = model.predict_all_events()
# 将 Prediction dataclass 转为可序列化 dictv5.1补充缺失字段)
predictions = []
for p in raw_preds:
predictions.append({
"event_type": str(p.event_type.value) if hasattr(p.event_type, 'value') else str(p.event_type),
"description": p.description,
"probability": p.probability,
"risk_level": str(p.risk_level.value) if hasattr(p.risk_level, 'value') else str(p.risk_level),
"confidence": str(p.confidence.value) if hasattr(p.confidence, 'value') else str(p.confidence),
"timing": p.timing,
"key_factors": p.key_factors,
"recommendations": p.recommendations,
"dasha_signals": p.dasha_signals,
"transit_signals": p.transit_signals,
"timing_windows": p.timing_windows,
})
return {
"method": "三层验证法(EventPredictionModel规则引擎)",
"engine": "event_prediction_model.py",
"event_type": evt,
"predictions": predictions,
"note": "基于规则引擎的三层验证法,替代LAM神经网络(准确率从0.17%大幅提升)"
}
except Exception as e:
# 降级到简化版
result = {"method": "三层验证法(简化版)", "fallback_reason": str(e),
"event_type": evt, "predictions": []}
planets = chart.get("planets", {})
indicators_map = {
"marriage": {"houses": [7], "karaka": "Venus", "cn": "婚姻"},
"career": {"houses": [10, 6], "karaka": "Sun", "cn": "职业"},
"wealth": {"houses": [2, 11], "karaka": "Jupiter", "cn": "财富"},
"health": {"houses": [6, 8, 12], "karaka": "Saturn", "cn": "健康"},
}
for ek, ei in indicators_map.items():
if evt != "all" and evt != ek: continue
found = []
for hn in ei["houses"]:
for pn, pd in planets.items():
if isinstance(pd, dict) and pd.get("house") == hn:
found.append({"planet": pn, "house": hn, "sign": pd.get("sign", ""), "status": pd.get("status", "中性")})
if found:
result["predictions"].append({"event": ei["cn"], "key": ek, "static_indicators": found, "note": "需要结合Dasha和Transit进行精确预测"})
return result
# ============================================================================
# 验前事模式(v3.5新增,避免冷读效应)
# ============================================================================
def _past_event_verify(chart: Dict, asc_idx: int, args) -> Dict:
"""
验前事:从星盘数据推断 2-4 个高信号历史时段,供用户确认。
AI 先推断,用户后确认——避免冷读效应。
"""
planets = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Unknown')
birth_year = args.year
signals = []
# 1. 土星回归(约29.5年一次)
saturn_sign = planets.get('Saturn', {}).get('sign', '')
saturn_house = planets.get('Saturn', {}).get('house', 0)
# 土星绕黄道一圈约29.5年
for cycle_age in [29, 58]:
event_year = birth_year + cycle_age
signals.append({
'type': '土星回归',
'age': cycle_age,
'year': event_year,
'description': f'{event_year}年({cycle_age}岁),土星回归周期',
'confidence': '',
'indicators': [f'土星在{saturn_sign}(第{saturn_house}宫)'],
})
# 2. 木星回归(约12年一次)
jupiter_sign = planets.get('Jupiter', {}).get('sign', '')
jupiter_house = planets.get('Jupiter', {}).get('house', 0)
for cycle_age in [12, 24, 36, 48]:
event_year = birth_year + cycle_age
signals.append({
'type': '木星回归',
'age': cycle_age,
'year': event_year,
'description': f'{event_year}年({cycle_age}岁),木星回归周期',
'confidence': '中高',
'indicators': [f'木星在{jupiter_sign}(第{jupiter_house}宫)'],
})
# 3. Rahu-Ketu 对冲过境(约18.6年半周期)
rahu_sign = planets.get('Rahu', {}).get('sign', '')
for half_cycle in [9, 18, 27, 36]:
event_year = birth_year + half_cycle
signals.append({
'type': 'Rahu-Ketu半周期',
'age': half_cycle,
'year': event_year,
'description': f'{event_year}年({half_cycle}岁),Rahu-Ketu对冲轴变化',
'confidence': '',
'indicators': [f'本命Rahu在{rahu_sign}'],
})
# 4. 关键宫位激活(基于 Dasha 可能性)
# 7宫主星相关 → 婚姻/合作时间窗
libra_idx = SIGNS.index('Libra') if 'Libra' in SIGNS else 6
sign_7 = SIGNS[(asc_idx + 6) % 12]
lord_7 = SIGN_LORDS.get(sign_7, 'Unknown')
lord_7_info = planets.get(lord_7, {})
if lord_7_info:
signals.append({
'type': '7宫主星活跃期',
'age_range': '24-32',
'year_range': f'{birth_year + 24}-{birth_year + 32}',
'description': f'{lord_7}7宫主星,7宫={sign_7})活跃期,可能涉及婚姻/重要合作',
'confidence': '',
'indicators': [f'{lord_7}{lord_7_info.get("sign", "")}(第{lord_7_info.get("house", 0)}宫)'],
})
# 5. 10宫主星相关 → 事业突破
sign_10 = SIGNS[(asc_idx + 9) % 12]
lord_10 = SIGN_LORDS.get(sign_10, 'Unknown')
lord_10_info = planets.get(lord_10, {})
if lord_10_info:
signals.append({
'type': '10宫主星活跃期',
'age_range': '28-40',
'year_range': f'{birth_year + 28}-{birth_year + 40}',
'description': f'{lord_10}10宫主星,10宫={sign_10})活跃期,可能涉及事业突破',
'confidence': '',
'indicators': [f'{lord_10}{lord_10_info.get("sign", "")}(第{lord_10_info.get("house", 0)}宫)'],
})
# 按置信度排序,取前4
priority = {'': 3, '中高': 2, '': 1, '': 0}
signals.sort(key=lambda s: priority.get(s.get('confidence', ''), 0), reverse=True)
top_signals = signals[:4]
return {
'method': '验前事(Past Event Reverse Verification',
'version': '3.5',
'note': 'AI从星盘推断的高信号历史时段,请用户确认——避免冷读效应',
'birth_year': birth_year,
'ascendant': asc_sign,
'inferred_periods': top_signals,
'disclaimer': '这些是基于星盘结构推断的可能时段,需要用户确认是否实际发生了相关事件。',
}
# ============================================================================
# 5. 分盘计算
# ============================================================================
def cmd_varga(args):
if not HAS_SWE: return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from varga import calc_varga
except ImportError as e:
return {"error": f"varga模块导入失败: {e}"}
swe.set_ephe_path('')
hd = _birth_hour_decimal(args.hour, args.minute, _arg_second(args)) - args.tz
jd = swe.julday(args.year, args.month, args.day, hd)
# Lahiri Ayanamsa(恒星黄道修正,与cmd_chart一致)
ayanamsa = swe.get_ayanamsa(jd)
natal = {}
for pn, pid in PLANETS_SWE.items():
pos, _ = swe.calc_ut(jd, pid); natal[pn] = (pos[0] - ayanamsa) % 360 # 恒星黄道
if 'Rahu' in natal: natal['Ketu'] = (natal['Rahu'] + 180) % 360
asc_lon, _ = swe.houses(jd, args.lat, args.lon, b'A'); asc_deg = (asc_lon[0] - ayanamsa) % 360 # 恒星黄道
def short_varga_row(lon, div):
row = calc_varga(lon, div)
return {"sign": row["sign"], "sign_cn": SIGNS_CN[row["sign"]]}
result = {"birth_info": f"{args.year}-{args.month:02d}-{args.day:02d} {_birth_time_string(args.hour, args.minute, _arg_second(args))}", "divisional_charts": {}}
if args.d9 or args.all:
d9 = {"ascendant": calc_varga(asc_deg, 9)["sign"]}
for p, l in natal.items(): d9[p] = short_varga_row(l, 9)
result["divisional_charts"]["D9_Navamsa"] = d9
if args.d10 or args.all:
d10 = {"ascendant": calc_varga(asc_deg, 10)["sign"]}
for p, l in natal.items(): d10[p] = short_varga_row(l, 10)
result["divisional_charts"]["D10_Dasamsa"] = d10
if not result["divisional_charts"]: result["note"] = "请指定 --d9, --d10 或 --all"
return result
# ============================================================================
# 6. 名人案例查询
# ============================================================================
def cmd_celebrity(args):
result = {"query": args.name or "all", "results": []}
if os.path.exists(DB_PATH):
try:
conn = sqlite3.connect(DB_PATH); c = conn.cursor()
if args.name: c.execute("SELECT * FROM cases WHERE name LIKE ?", (f'%{args.name}%',))
else: c.execute("SELECT * FROM cases LIMIT ?", (args.limit or 20,))
cols = [d[0] for d in c.description]
for r in c.fetchall(): result["results"].append(dict(zip(cols, r)))
conn.close()
except Exception as e: result["db_error"] = str(e)
if os.path.exists(PERSON_CSV) and args.name:
try:
matches = []
with open(PERSON_CSV, 'r', encoding='utf-8') as f:
for row in csv.DictReader(f):
if args.name.lower() in row.get('Name', '').lower():
matches.append({"name": row.get('Name', ''), "birth_time": row.get('BirthTime', ''), "gender": row.get('Gender', '')})
if len(matches) >= 10: break
result["person_list_matches"] = matches; result["person_list_total"] = 15807
except Exception as e: result["csv_error"] = str(e)
return result
# ============================================================================
# 7. 数据库统计
# ============================================================================
def cmd_db_stats(args):
result = {}
if os.path.exists(DB_PATH):
try:
conn = sqlite3.connect(DB_PATH); c = conn.cursor()
c.execute("SELECT COUNT(*) FROM cases"); result["total_cases"] = c.fetchone()[0]
c.execute("SELECT case_type, COUNT(*) FROM cases GROUP BY case_type"); result["by_type"] = dict(c.fetchall())
c.execute("SELECT difficulty, COUNT(*) FROM cases GROUP BY difficulty"); result["by_difficulty"] = dict(c.fetchall())
c.execute("SELECT name, accuracy_rate, sample_size, correct_predictions FROM techniques"); result["techniques"] = [{"name": r[0], "accuracy": r[1], "samples": r[2], "correct": r[3]} for r in c.fetchall()]
conn.close()
except Exception as e: result["error"] = str(e)
else:
result["error"] = f"数据库不存在: {DB_PATH}"
return result
# ============================================================================
# 8. 过境查询
# ============================================================================
def cmd_transit(args):
"""
实时 Transit 行星过境计算(v3.7.2 改用 Swiss Ephemeris
不再依赖静态 JSON,直接用 swe 计算任意日期的行星位置。
支持指定日期范围(--year/--month)和目标行星(--planet)。
"""
if not HAS_SWE:
return {"error": "swisseph未安装,无法计算实时Transit"}
swe.set_ephe_path('')
# --- 参数解析 ---
t_year = args.year
t_month = args.month
# 可选:指定某日(默认取月中15号做代表)
t_day = getattr(args, 'day', 15) or 15
# 可选:指定目标行星(默认全部七曜+Rahu/Ketu)
target_planets_str = getattr(args, 'planet', None)
target_planets = [p.strip() for p in target_planets_str.split(',')] if target_planets_str else None
# --- 计算该月月中行星位置 ---
try:
hd = 12.0 - (getattr(args, 'tz', 8) or 8) # 默认UTC+8中午
jd_mid = swe.julday(t_year, t_month, t_day, hd)
except Exception as e:
return {"error": f"日期计算失败: {e}"}
ayanamsa = swe.get_ayanamsa(jd_mid)
# --- 计算行星位置 ---
node_mode = getattr(args, 'node_mode', 'mean')
transit_data, ayanamsa = _calc_sidereal_planets_for_jd(jd_mid, node_mode=node_mode, include_ketu=True)
if target_planets:
target_set = set(target_planets)
transit_data = {p: d for p, d in transit_data.items() if p in target_set}
# --- 计算行星间相位(互相在对方星座的宫位关系)---
aspects_found = []
planet_names = list(transit_data.keys())
for i, p1 in enumerate(planet_names):
for p2 in planet_names[i+1:]:
if p1 == 'Ketu' or p2 == 'Ketu':
continue
d1 = transit_data[p1]['degree']
d2 = transit_data[p2]['degree']
diff = abs(d1 - d2) % 360
if diff > 180:
diff = 360 - diff
# 合相(≤10°)
if diff <= 10:
aspects_found.append({
'type': 'conjunction',
'planets': [p1, p2],
'degree_diff': round(diff, 2),
'description': f'{p1}{p2}合相({diff:.1f}°)'
})
# 对冲(180°±8°)
elif 172 <= diff <= 188:
aspects_found.append({
'type': 'opposition',
'planets': [p1, p2],
'degree_diff': round(diff, 2),
'description': f'{p1}{p2}对冲({diff:.1f}°)'
})
# 三方(120°±6°)
elif 114 <= diff <= 126:
aspects_found.append({
'type': 'trine',
'planets': [p1, p2],
'degree_diff': round(diff, 2),
'description': f'{p1}{p2}三方相位({diff:.1f}°)'
})
# 四分(90°±6°)
elif 84 <= diff <= 96:
aspects_found.append({
'type': 'square',
'planets': [p1, p2],
'degree_diff': round(diff, 2),
'description': f'{p1}{p2}四分相位({diff:.1f}°)'
})
# --- 构建结果 ---
result = {
'method': 'Swiss Ephemeris 实时计算(v3.7.2',
'target_date': f'{t_year}-{t_month:02d}-{t_day:02d}',
'ayanamsa': round(ayanamsa, 4),
'node_mode': node_mode,
'data_layer': 'true_transit_positions',
'planets': transit_data,
'aspects': aspects_found,
'note': f'使用Swiss Ephemeris实时计算{t_year}{t_month}月行星过境位置,不再依赖静态JSON'
}
return result
# ============================================================================
# 8b. Double Transit PAC + D9 层(KN Rao 完整实现 v3.9新增)
#
# 核心逻辑:
# 1. D1 层: Saturn/Jupiter 通过 PAC 关联事件宫/宫主/LL/对宫主
# 2. D9 层: Saturn/Jupiter 通过 PAC 关联 D9 宫主/D9 Asc/宫主D9星座/LL D9星座
# 3. 两者必须同时激活同一目标 -> Double Transit 确认
#
# 精度: KN Rao 体系 110-115 星盘测试 97% 准确率(使用 D9 Navamsa
# ============================================================================
def _navamsa_idx(lon):
"""Navamsa 星座索引"""
lon = lon % 360
si = int(lon / 30)
d = lon - si * 30
ni = int(d / (30 / 9))
el_starts = [0, 9, 6, 3] # Aries/Fire=0, Taurus/Earth=9, Gemini/Air=6, Cancer/Water=3
return (el_starts[si % 4] + ni) % 12
def _calc_planetary_congregation(planets: Dict, asc_idx: int) -> Dict:
"""
本命盘行星聚集检测(供 cmd_full_reading 调用)
检测 3+ 行星同宫的聚集效应,返回聚集宫位、行星列表、影响领域
"""
houses = {}
for pn, pd in planets.items():
if not isinstance(pd, dict) or 'sign' not in pd:
continue
if pn in ['Rahu', 'Ketu']:
continue # Rahu/Ketu 不计入聚集
si = SIGNS.index(pd['sign']) if pd['sign'] in SIGNS else 0
h = ((si - asc_idx) % 12) + 1
houses.setdefault(h, []).append(pn)
congregations = []
for h, plist in houses.items():
if len(plist) >= 3:
h_sign = SIGNS[(asc_idx + h - 1) % 12]
# 判断影响领域
impact = _house_theme(h)
# 判断聚集力量(有无吉星/凶星)
benefics = [p for p in plist if p in ['Jupiter', 'Venus', 'Mercury', 'Moon']]
malefics = [p for p in plist if p in ['Saturn', 'Mars', 'Sun', 'Rahu']]
strength = 'strong' if len(benefics) > len(malefics) else 'mixed'
if len(malefics) >= 3:
strength = 'malefic_heavy'
congregations.append({
'house': h,
'sign': h_sign,
'planets': plist,
'count': len(plist),
'benefics': benefics,
'malefics': malefics,
'strength': strength,
'impact': impact,
'description': f'{",".join(plist)} 聚集于{h}宫({h_sign})',
})
return {
'congregations': congregations,
'total': len(congregations),
'note': '3+ 行星同宫为显著聚集,影响该宫主题领域',
}
def _house_theme(house: int) -> List[str]:
"""返回宫位影响领域"""
themes = {
1: ['自我', '健康', '性格'],
2: ['财富', '家庭', '言语'],
3: ['沟通', '旅行', '兄弟'],
4: ['母亲', '房产', '情感'],
5: ['子女', '投资', '创意'],
6: ['疾病', '敌人', '债务'],
7: ['婚姻', '合作', '伴侣'],
8: ['转型', '意外', '遗产'],
9: ['命运', '父亲', '灵性'],
10: ['事业', '声望', '成就'],
11: ['收益', '社交', '愿望'],
12: ['损失', '外迁', '解脱'],
}
return themes.get(house, ['未知'])
def _calc_vivah_saham(planets: Dict, asc_deg: float) -> Dict:
"""
计算本命 Vivah Saham 婚姻敏感点(供 cmd_full_reading 调用)
公式: Saham = (Venus_lon - Saturn_lon + Asc_deg) % 360
"""
venus_lon = planets.get('Venus', {}).get('degree', 0)
saturn_lon = planets.get('Saturn', {}).get('degree', 0)
if venus_lon == 0 or saturn_lon == 0:
return {'error': '缺少金星或土星数据', 'saham': None}
sahams_lon = (venus_lon - saturn_lon + asc_deg) % 360
sahams_sign = SIGNS[int(sahams_lon / 30) % 12]
sahams_deg_in_sign = sahams_lon % 30
sahams_si = int(sahams_lon / 30) % 12
# 检查哪些本命行星与 Saham 同宫/合相
conjuncts = []
for pn, pd in planets.items():
if pn in ['Rahu', 'Ketu']:
continue
if not isinstance(pd, dict) or 'degree' not in pd:
continue
p_lon = pd['degree']
diff = abs(p_lon - sahams_lon) % 360
if diff > 180:
diff = 360 - diff
if diff <= 5:
conjuncts.append({'planet': pn, 'diff_deg': round(diff, 2)})
# 从 Saham 位置反推婚姻相关宫位
asc_si = int(asc_deg / 30) % 12
sahams_house = ((sahams_si - asc_si) % 12) + 1
return {
'saham_lon': round(sahams_lon, 4),
'saham_sign': sahams_sign,
'saham_deg_in_sign': round(sahams_deg_in_sign, 2),
'saham_house': sahams_house,
'formula': f'Venus({venus_lon:.2f}°) - Saturn({saturn_lon:.2f}°) + Asc({asc_deg:.2f}°)',
'natal_conjuncts': conjuncts,
'marriage_relevance': 'high' if sahams_house in [7, 1, 5, 9] else 'moderate',
'note': 'Vivah Saham 是度数级婚姻敏感点,Transit 木星/土星过境此点时触发婚姻事件窗',
}
def _check_pac(planet_name, planet_lon, target_lon, asc_idx):
"""PAC检查: Position(同宫)/Aspect(相位)/Conjunction(合相<=10度)"""
results = []
p_si = int((planet_lon % 360) / 30)
t_si = int((target_lon % 360) / 30)
p_house = ((p_si - asc_idx) % 12) + 1
t_house = ((t_si - asc_idx) % 12) + 1
# P: Position - 同宫
if p_house == t_house:
results.append({'type': 'Position', 'desc': f'同宫({t_house}宫)'})
# C: Conjunction - 合相 <=10度
diff = abs(planet_lon - target_lon) % 360
if diff > 180:
diff = 360 - diff
if diff <= 10:
results.append({'type': 'Conjunction', 'desc': f'合相({diff:.2f}\u00b0)'})
# A: Aspect - Graha Drishti
planet_aspects = {
'Sun': [7], 'Moon': [7], 'Mars': [4, 7, 8], 'Mercury': [7],
'Jupiter': [5, 7, 9], 'Venus': [7], 'Saturn': [3, 7, 10],
'Rahu': [5, 7, 9], 'Ketu': [5, 7, 9],
}
aspects = planet_aspects.get(planet_name, [7])
for offset in aspects:
if ((t_house - p_house + 12) % 12) == offset:
results.append({'type': 'Aspect', 'offset': offset, 'desc': f'{offset}宫相位'})
return results
def cmd_double_transit_pac(args):
"""Double Transit PAC + D9 层计算"""
# 1. 计算本命星盘
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Aries')
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
event_house = args.house or 7
# 2. 计算过境行星位置
if not HAS_SWE:
return {"error": "swisseph未安装"}
transit_year, transit_month, transit_day = map(int, args.date.split('-'))
transit_hour = 12.0 - args.tz # 正午 UT
transit_jd = swe.julday(transit_year, transit_month, transit_day, transit_hour)
transit_ayanamsa = swe.get_ayanamsa(transit_jd)
transit_planets = {}
for pname, pid in PLANETS_SWE.items():
try:
pos, _ = swe.calc_ut(transit_jd, pid)
lon_p = (pos[0] - transit_ayanamsa) % 360
transit_planets[pname] = {'lon': lon_p, 'sign': SIGNS[int(lon_p / 30)]}
if pname == 'Rahu':
klon = (lon_p + 180) % 360
transit_planets['Ketu'] = {'lon': klon, 'sign': SIGNS[int(klon / 30)]}
except Exception as e:
transit_planets[pname] = {'error': str(e)}
# 3. D1 层敏感点
event_si = (asc_idx + event_house - 1) % 12
event_sign = SIGNS[event_si]
event_lord = SIGN_LORDS[event_sign]
ll_name = SIGN_LORDS[asc_sign]
opposite_si = (asc_idx + 6) % 12
opposite_lord = SIGN_LORDS[SIGNS[opposite_si]]
event_house_lon = (event_si * 30) + 15 # 宫位中点
ll_lon = natal.get(ll_name, {}).get('degree', 0)
event_lord_lon = natal.get(event_lord, {}).get('degree', 0)
opp_lord_lon = natal.get(opposite_lord, {}).get('degree', 0)
d1_targets = {
f'{event_house}宫({event_sign})': event_house_lon,
f'{event_lord}(宫主)': event_lord_lon,
f'{ll_name}(LL)': ll_lon,
f'{opposite_lord}(对宫主)': opp_lord_lon,
}
# 4. D9 层敏感点
d9_asc_idx = _navamsa_idx(asc_deg)
d9_asc_sign = SIGNS[d9_asc_idx]
d9_event_si = (d9_asc_idx + event_house - 1) % 12
d9_event_sign = SIGNS[d9_event_si]
d9_event_lord = SIGN_LORDS[d9_event_sign]
# 宫主的 D9 星座(KN Rao 关键)
event_lord_d9_si = _navamsa_idx(event_lord_lon)
event_lord_d9_sign = SIGNS[event_lord_d9_si]
ll_d9_si = _navamsa_idx(ll_lon)
ll_d9_sign = SIGNS[ll_d9_si]
d9_event_house_lon = (d9_asc_idx * 30) + 15
d9_event_lord_lon = natal.get(d9_event_lord, {}).get('degree', 0)
d9_targets = {
f'D9_{event_house}宫({d9_event_sign})': d9_event_house_lon,
f'D9_{d9_event_lord}(宫主)': d9_event_lord_lon,
f'{event_lord}_D9({event_lord_d9_sign})': event_lord_d9_si * 30 + 15,
f'{ll_name}_D9({ll_d9_sign})': ll_d9_si * 30 + 15,
}
# 5. PAC 检查
results = {
'transit_date': args.date,
'event_house': event_house,
'd1': {'jupiter': {}, 'saturn': {}},
'd9': {'jupiter': {}, 'saturn': {}},
'cl': {'jupiter': {}, 'saturn': {}},
'double_transit': [],
'summary': '',
}
# Chandra Lagna 层敏感点
moon_lon = natal.get('Moon', {}).get('degree', 0)
moon_idx = int((moon_lon % 360) / 30)
cl_event_sign = SIGNS[(moon_idx + event_house - 1) % 12]
cl_event_lord = SIGN_LORDS[cl_event_sign]
cl_event_house_lon = ((moon_idx + event_house - 1) % 12) * 30 + 15
cl_event_lord_lon = natal.get(cl_event_lord, {}).get('degree', 0)
cl_targets = {
f'CL_{event_house}宫({cl_event_sign})': cl_event_house_lon,
f'CL_{cl_event_lord}(宫主)': cl_event_lord_lon,
}
for tp_name in ['Jupiter', 'Saturn']:
tp = transit_planets.get(tp_name, {})
if 'error' in tp:
continue
tp_lon = tp['lon']
layer = tp_name.lower()
for t_name, t_lon in d1_targets.items():
pac = _check_pac(tp_name, tp_lon, t_lon, asc_idx)
if pac:
results['d1'][layer][t_name] = pac
for t_name, t_lon in d9_targets.items():
pac = _check_pac(tp_name, tp_lon, t_lon, d9_asc_idx)
if pac:
results['d9'][layer][t_name] = pac
for t_name, t_lon in cl_targets.items():
pac = _check_pac(tp_name, tp_lon, t_lon, moon_idx)
if pac:
results['cl'][layer][t_name] = pac
# 6. Double Transit 判定
jup_d1 = set(results['d1']['jupiter'].keys())
sat_d1 = set(results['d1']['saturn'].keys())
jup_d9 = set(results['d9']['jupiter'].keys())
sat_d9 = set(results['d9']['saturn'].keys())
jup_cl = set(results['cl']['jupiter'].keys())
sat_cl = set(results['cl']['saturn'].keys())
# D1 层 overlap
d1_overlap = jup_d1 & sat_d1
for t in d1_overlap:
results['double_transit'].append({
'layer': 'D1', 'target': t,
'jupiter_pac': results['d1']['jupiter'][t],
'saturn_pac': results['d1']['saturn'][t],
'strength': 'strong',
})
# D9 层 overlap
d9_overlap = jup_d9 & sat_d9
for t in d9_overlap:
results['double_transit'].append({
'layer': 'D9', 'target': t,
'jupiter_pac': results['d9']['jupiter'][t],
'saturn_pac': results['d9']['saturn'][t],
'strength': 'strong',
})
# Chandra Lagna 层 overlap
cl_overlap = jup_cl & sat_cl
for t in cl_overlap:
results['double_transit'].append({
'layer': 'CL', 'target': t,
'jupiter_pac': results['cl']['jupiter'][t],
'saturn_pac': results['cl']['saturn'][t],
'strength': 'strong',
})
# 跨层 Double Transit
for d1t in jup_d1:
for d9t in sat_d9:
d1_nums = ''.join(c for c in d1t if c.isdigit())
d9_nums = ''.join(c for c in d9t if c.isdigit())
if d1_nums == d9_nums or (event_lord in d1t and event_lord in d9t):
results['double_transit'].append({
'layer': 'D1+D9', 'target': f'Jupiter(D1){d1t} + Saturn(D9){d9t}',
'jupiter_pac': results['d1']['jupiter'][d1t],
'saturn_pac': results['d9']['saturn'][d9t],
'strength': 'moderate',
})
for d1t in sat_d1:
for d9t in jup_d9:
d1_nums = ''.join(c for c in d1t if c.isdigit())
d9_nums = ''.join(c for c in d9t if c.isdigit())
if d1_nums == d9_nums or (event_lord in d1t and event_lord in d9t):
results['double_transit'].append({
'layer': 'D1+D9', 'target': f'Saturn(D1){d1t} + Jupiter(D9){d9t}',
'jupiter_pac': results['d9']['jupiter'][d9t],
'saturn_pac': results['d1']['saturn'][d1t],
'strength': 'moderate',
})
# Summary
d1_active = len(d1_overlap) > 0
d9_active = len(d9_overlap) > 0
cl_active = len(cl_overlap) > 0
cross_active = any(d['layer'] == 'D1+D9' for d in results['double_transit'])
active_layers = []
if d1_active: active_layers.append('D1')
if d9_active: active_layers.append('D9')
if cl_active: active_layers.append('CL')
if len(active_layers) >= 2:
results['summary'] = f'✅ Double Transit PAC 确认: {"+".join(active_layers)} 多层激活{event_house}宫主题'
elif d1_active:
results['summary'] = f'⚠️ D1 层 Double Transit 激活,D9/CL 层未确认'
elif d9_active:
results['summary'] = f'⚠️ D9 层 Double Transit 激活,D1/CL 层未确认'
elif cl_active:
results['summary'] = f'⚠️ Chandra Lagna 层 Double Transit 激活,D1/D9 未确认'
elif cross_active:
results['summary'] = f'⚠️ 跨层间接 Double Transit (D1+D9),需结合 Dasha 确认'
else:
results['summary'] = f'❌ 无 Double Transit PAC 激活'
results['stats'] = {
'd1_jupiter_targets': sorted(jup_d1),
'd1_saturn_targets': sorted(sat_d1),
'd9_jupiter_targets': sorted(jup_d9),
'd9_saturn_targets': sorted(sat_d9),
'cl_jupiter_targets': sorted(jup_cl),
'cl_saturn_targets': sorted(sat_cl),
'd1_overlap': sorted(d1_overlap),
'd9_overlap': sorted(d9_overlap),
'cl_overlap': sorted(cl_overlap),
'd9_ascendant': d9_asc_sign,
'event_lord_d9_sign': event_lord_d9_sign,
'chandra_lagna': SIGNS[moon_idx],
}
return results
# ============================================================================
# 8c. Transit LL/7L 连接 + 互换(Parivartana)(v3.9新增)
#
# P5: Transit LL PAC natal 7L / Transit 7L PAC natal LL (98%命中率)
# P8: Transit LL 过 7H 或 Transit 7L 过 Lagna (59%命中率)
# + Parivartana 互换检测
# ============================================================================
def _calc_transit_lon(jd, planet_name):
"""计算指定 Julian Day 的行星恒星黄经"""
pid_map = {'Sun': swe.SUN, 'Moon': swe.MOON, 'Mars': swe.MARS, 'Mercury': swe.MERCURY,
'Jupiter': swe.JUPITER, 'Venus': swe.VENUS, 'Saturn': swe.SATURN, 'Rahu': swe.MEAN_NODE}
pid = pid_map.get(planet_name)
if pid is None:
return None
pos, _ = swe.calc_ut(jd, pid)
aya = swe.get_ayanamsa_ut(jd)
return (pos[0] - aya) % 360
def cmd_transit_ll7l(args):
"""Transit LL/7L 连接 + 互换检测"""
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Aries')
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
ll_name = SIGN_LORDS[asc_sign]
seven_sign = SIGNS[(SIGNS.index(asc_sign) + 6) % 12]
seven_lord = SIGN_LORDS[seven_sign]
# Transit 日期
t_year, t_month, t_day = map(int, args.date.split('-'))
transit_jd = swe.julday(t_year, t_month, t_day, 12.0 - args.tz)
# Transit LL/7L 位置
t_ll_lon = _calc_transit_lon(transit_jd, ll_name)
t_7l_lon = _calc_transit_lon(transit_jd, seven_lord)
if t_ll_lon is None or t_7l_lon is None:
return {"error": f"无法计算 Transit 位置: LL={ll_name}, 7L={seven_lord}"}
n_ll_lon = natal.get(ll_name, {}).get('degree', 0)
n_7l_lon = natal.get(seven_lord, {}).get('degree', 0)
asc_lon = asc_deg
result = {
'transit_date': args.date,
'lagna_lord': ll_name,
'seventh_lord': seven_lord,
'p5': {'hit': False, 'details': []},
'p8': {'hit': False, 'details': []},
'parivartana': {'hit': False, 'details': []},
}
# P5: Transit LL PAC natal 7L / Transit 7L PAC natal LL
pac1 = _check_pac(ll_name, t_ll_lon, n_7l_lon, asc_idx)
if pac1:
result['p5']['hit'] = True
result['p5']['details'].append({
'direction': f'Transit {ll_name} → natal {seven_lord}',
'connections': pac1,
})
pac2 = _check_pac(seven_lord, t_7l_lon, n_ll_lon, asc_idx)
if pac2:
result['p5']['hit'] = True
result['p5']['details'].append({
'direction': f'Transit {seven_lord} → natal {ll_name}',
'connections': pac2,
})
# P8: Transit LL 过 7H 或 Transit 7L 过 Lagna
t_ll_house = ((int((t_ll_lon % 360) / 30) - asc_idx) % 12) + 1
t_7l_house = ((int((t_7l_lon % 360) / 30) - asc_idx) % 12) + 1
if t_ll_house == 7:
result['p8']['hit'] = True
result['p8']['details'].append(f'Transit {ll_name}({SIGNS[int(t_ll_lon/30)]})在7H')
if t_7l_house == 1:
result['p8']['hit'] = True
result['p8']['details'].append(f'Transit {seven_lord}({SIGNS[int(t_7l_lon/30)]})在Lagna')
# Parivartana 互换
n_ll_sign = SIGNS[int((n_ll_lon % 360) / 30)]
n_7l_sign = SIGNS[int((n_7l_lon % 360) / 30)]
t_ll_in_7l = SIGNS[int((t_ll_lon % 360) / 30)] == n_7l_sign
t_7l_in_ll = SIGNS[int((t_7l_lon % 360) / 30)] == n_ll_sign
if t_ll_in_7l and t_7l_in_ll:
result['parivartana']['hit'] = True
result['parivartana']['details'].append(
f'完整互换: Transit {ll_name}{n_7l_sign}(natal {seven_lord}) + Transit {seven_lord}{n_ll_sign}(natal {ll_name})')
elif t_ll_in_7l:
result['parivartana']['details'].append(f'部分: Transit {ll_name}{n_7l_sign}')
elif t_7l_in_ll:
result['parivartana']['details'].append(f'部分: Transit {seven_lord}{n_ll_sign}')
return result
# ============================================================================
# 8d. 行星聚集检测(Lagna/7H + Transit 聚集)(v3.9新增)
# ============================================================================
def cmd_planetary_congregation(args):
"""行星聚集检测"""
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Aries')
event_house = args.house or 7
result = {
'natal': {'lagna': [], 'house_7': [], f'house_{event_house}': []},
'transit': None,
'summary': '',
}
# 本命盘聚集
for pname, pdata in natal.items():
if 'error' in pdata or 'sign' not in pdata:
continue
p_si = SIGNS.index(pdata['sign'])
house = ((p_si - asc_idx) % 12) + 1
if house == 1:
result['natal']['lagna'].append(pname)
if house == 7:
result['natal']['house_7'].append(pname)
if house == event_house:
result['natal'][f'house_{event_house}'].append(pname)
# Transit 聚集
if args.transit_date:
t_year, t_month, t_day = map(int, args.transit_date.split('-'))
transit_jd = swe.julday(t_year, t_month, t_day, 12.0 - args.tz)
transit_aya = swe.get_ayanamsa(transit_jd)
result['transit'] = {str(h): [] for h in range(1, 13)}
for pname, pid in PLANETS_SWE.items():
try:
pos, _ = swe.calc_ut(transit_jd, pid)
lon_p = (pos[0] - transit_aya) % 360
si = int(lon_p / 30)
house = ((si - asc_idx) % 12) + 1
result['transit'][str(house)].append(pname)
if pname == 'Rahu':
klon = (lon_p + 180) % 360
ksi = int(klon / 30)
khouse = ((ksi - asc_idx) % 12) + 1
result['transit'][str(khouse)].append('Ketu')
except Exception:
pass
# 判定
flags = []
lagna_count = len(result['natal']['lagna'])
h7_count = len(result['natal']['house_7'])
if 'Sun' in result['natal']['lagna'] or lagna_count >= 3:
flags.append(f"Lagna聚集: {','.join(result['natal']['lagna'])}({lagna_count})")
if 'Sun' in result['natal']['house_7'] or h7_count >= 3:
flags.append(f"7H聚集: {','.join(result['natal']['house_7'])}({h7_count})")
if result['transit']:
slow = {'Saturn', 'Jupiter', 'Rahu', 'Ketu'}
t_event = result['transit'].get(str(event_house), [])
t_slow = [p for p in t_event if p in slow]
if len(t_slow) >= 2:
flags.append(f"Transit {event_house}宫慢行星聚集: {','.join(t_slow)}")
result['flags'] = flags
result['hit'] = len(flags) > 0
result['summary'] = ' | '.join(flags) if flags else '无显著聚集'
return result
# ============================================================================
# 8e. Vivah Saham + 婚姻计时管线(v3.9新增)
#
# Vivah Saham = norm(Venus - Saturn + Asc) — 度数级精确计算
# Transit 激活: Jupiter/Saturn PAC 到 Vivah Saham
# ============================================================================
def cmd_vivah_saham(args):
"""Vivah Saham 计算 + Transit 激活"""
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
natal = chart.get('planets', {})
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
venus_lon = natal.get('Venus', {}).get('degree', 0)
saturn_lon = natal.get('Saturn', {}).get('degree', 0)
# Vivah Saham = norm(Venus - Saturn + Asc)
sahams_lon = (venus_lon - saturn_lon + asc_deg) % 360
sahams_si = int(sahams_lon / 30)
sahams_sign = SIGNS[sahams_si]
sahams_deg = sahams_lon - sahams_si * 30
result = {
'vivah_saham': {
'longitude': round(sahams_lon, 4),
'sign': sahams_sign,
'sign_cn': SIGNS_CN[sahams_sign],
'degree_in_sign': round(sahams_deg, 4),
},
'formula': f'norm({venus_lon:.2f} Venus - {saturn_lon:.2f} Saturn + {asc_deg:.2f} Asc)',
'transit_activation': None,
}
# Transit 激活
if args.transit_date:
t_year, t_month, t_day = map(int, args.transit_date.split('-'))
transit_jd = swe.julday(t_year, t_month, t_day, 12.0 - args.tz)
result['transit_activation'] = {'jupiter': [], 'saturn': [], 'double_activation': False}
jup_lon = _calc_transit_lon(transit_jd, 'Jupiter')
sat_lon = _calc_transit_lon(transit_jd, 'Saturn')
if jup_lon is not None:
jup_pac = _check_pac('Jupiter', jup_lon, sahams_lon, asc_idx)
if jup_pac:
result['transit_activation']['jupiter'] = jup_pac
if sat_lon is not None:
sat_pac = _check_pac('Saturn', sat_lon, sahams_lon, asc_idx)
if sat_pac:
result['transit_activation']['saturn'] = sat_pac
if result['transit_activation']['jupiter'] and result['transit_activation']['saturn']:
result['transit_activation']['double_activation'] = True
# Venus transit 过 Saham 星座
venus_t = _calc_transit_lon(transit_jd, 'Venus')
if venus_t is not None:
if SIGNS[int(venus_t / 30)] == sahams_sign:
result['transit_activation']['venus_in_saham_sign'] = True
return result
# ============================================================================
# 9. Shadbala 六重力量(v3.4新增)
# ============================================================================
def cmd_shadbala(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from shadbala import build_shadbala_context, calc_shadbala
except ImportError as e:
return {"error": f"shadbala模块导入失败: {e}"}
planets = chart.get("planets", {})
asc_sign = chart.get("ascendant", {}).get("sign", "Aries")
birth_hour = _birth_hour_decimal(args.hour, args.minute, _arg_second(args))
sun_lon = planets.get("Sun", {}).get("degree", 0)
moon_lon = planets.get("Moon", {}).get("degree", 0)
context = build_shadbala_context(jd, args.lat, args.lon, _current_ayanamsa_name(args), args.tz)
return calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon, context=context)
# ============================================================================
# 10. Ashtakavarga 八分法(v3.4新增)
# ============================================================================
def cmd_ashtakavarga(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
except ImportError as e:
return {"error": f"ashtakavarga模块导入失败: {e}"}
planets = chart.get("planets", {})
return calc_ashtakavarga(planets, asc_idx)
# ============================================================================
# 10c. Ashtakoot 合婚(v6.9.12新增)
# ============================================================================
def cmd_ashtakoot(args):
"""Ashtakoot 36点合婚 + Kuja Dosha"""
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakoot import calculate_ashtakoot
except ImportError as e:
return {"error": f"ashtakoot模块导入失败: {e}"}
# 男方星盘
m_chart, m_asc_idx, m_jd, m_aya = compute_chart_data(
args.m_year, args.m_month, args.m_day, args.m_hour, args.m_minute,
args.m_lat, args.m_lon, args.m_tz, getattr(args, 'node_mode', 'mean'))
if m_chart is None:
return {"error": "男方: swisseph未安装"}
# 女方星盘
f_chart, f_asc_idx, f_jd, f_aya = compute_chart_data(
args.f_year, args.f_month, args.f_day, args.f_hour, args.f_minute,
args.f_lat, args.f_lon, args.f_tz, getattr(args, 'node_mode', 'mean'))
if f_chart is None:
return {"error": "女方: swisseph未安装"}
m_moon_lon = m_chart.get("planets", {}).get("Moon", {}).get("degree", 0)
f_moon_lon = f_chart.get("planets", {}).get("Moon", {}).get("degree", 0)
# 构建 lagna 数据供附加 Kuta 使用
m_chart_full = {"lagna": m_chart.get("ascendant", {}), "planets": m_chart.get("planets", {})}
f_chart_full = {"lagna": f_chart.get("ascendant", {}), "planets": f_chart.get("planets", {})}
return calculate_ashtakoot(m_moon_lon, f_moon_lon, m_chart_full, f_chart_full)
# ============================================================================
# 10b. KP 系统(v6.9.10新增)
# ============================================================================
def cmd_kp(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
asc_sign = chart.get("ascendant", {}).get("sign", "Aries")
planets = chart.get("planets", {})
# 构建KP需要的行星位置格式
planet_positions = {}
for pname, pdata in planets.items():
if isinstance(pdata, dict) and 'sign' in pdata:
planet_positions[pname] = {
'sign': pdata['sign'],
'degree': pdata.get('degree_in_sign', pdata.get('degree', 0) % 30),
'house': pdata.get('house', 1),
}
return calc_kp_analysis(planet_positions, asc_sign)
# ============================================================================
# 11. Hermes 记忆系统(v3.4新增)
# ============================================================================
def cmd_memory(args):
try:
sys.path.insert(0, SCRIPT_DIR)
from hermes_memory_core import HermesMemoryCore
except ImportError as e:
return {"error": f"Hermes记忆模块导入失败: {e}", "hint": "确保hermes_memory_core.py在scripts/目录下"}
db_file = os.path.join(SCRIPT_DIR, 'hermes_memory.db')
mem = HermesMemoryCore(db_file)
result = {"action": args.action}
if args.action == "store":
if not args.content:
return {"error": "store操作需要 --content 参数"}
tags = args.tags.split(',') if args.tags else []
importance = args.importance if args.importance else 5
metadata = {"tags": tags, "importance": importance}
mem_id = mem.store_memory(args.content, metadata)
result.update({"stored": True, "memory_id": mem_id, "content": args.content, "tags": tags})
elif args.action == "search":
if not args.query:
return {"error": "search操作需要 --query 参数"}
results = mem.search(args.query, limit=args.limit or 10)
result.update({"query": args.query, "found": len(results), "results": results})
elif args.action == "context":
session_id = f"jyotish-cli-{datetime.now().strftime('%Y%m%d%H%M%S')}"
ctx = mem.get_context_for_session(session_id)
result.update({"session_id": session_id, "context": ctx})
elif args.action == "stats":
# Hermes没有get_stats,用搜索空串获取总数
try:
all_mem = mem.search("", limit=1000)
result["total_memories"] = len(all_mem)
except:
result["total_memories"] = "unknown"
result["db_path"] = db_file
else:
result["error"] = f"未知action: {args.action},支持: store/search/context/stats"
return result
# ============================================================================
# 12. R1-R10 数学验证(v3.5新增)
# ============================================================================
def cmd_validate(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
asht_result = calc_ashtakavarga(chart.get('planets', {}), asc_idx)
except ImportError:
asht_result = None
try:
from validate import validate_chart
return validate_chart(chart, asht_result)
except ImportError as e:
return {"error": f"validate模块导入失败: {e}"}
# ============================================================================
# 13. P1-P12 行星审计管线(v3.5新增)
# ============================================================================
def _assess_conjunction_quality(lord, houses, planets):
"""评估仓库耦合的吉凶质量"""
lord_info = planets.get(lord, {})
lord_status = lord_info.get('status', '中性')
# 凶宫组合
dusthana = {6, 8, 12}
trikona = {1, 5, 9}
kendra = {1, 4, 7, 10}
has_dusthana = any(h in dusthana for h in houses)
has_trikona = any(h in trikona for h in houses)
has_kendra = any(h in kendra for h in houses)
if has_dusthana and has_trikona:
return f"凶吉混合 — 挑战与成长并存"
elif has_dusthana and has_kendra:
return f"压力型 — 通过努力获取成就"
elif has_trikona:
return f"吉庆型 — 自然流畅的支持"
elif has_dusthana:
return f"消耗型 — 需要额外努力维持"
else:
return f"中性 — 标准互动"
def _conflict_arbitration(report):
"""
冲突仲裁规则(CNWU16框架):
1. P1清理者+P7入旺 = "带毒高价值资产",禁止说逢凶化吉
2. P5凶宫+BAV高 = "乱世出英雄"
3. P1吉+P2受损 = "空有雄心无着力点"
"""
conflicts = []
planets = report.get('planets', {})
audit = report.get('audit', {})
p1 = audit.get('P1_identity', {})
p7 = audit.get('P7_dignity', {})
p2 = audit.get('P2_health', {})
asc_lord = p1.get('asc_lord', '')
# 规则1: P1清理者+P7入旺 → 检查上升主是否掌管8/12宫(清理者角色)
# 清理者定义:掌管8宫或12宫的行星
SIGN_LORDS_MAP = {'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon',
'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars',
'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'}
asc_sign = p1.get('asc_sign', '')
asc_idx_local = SIGNS.index(asc_sign) if asc_sign in SIGNS else 0
# 找8宫主和12宫主
h8_sign = SIGNS[(asc_idx_local + 7) % 12]
h12_sign = SIGNS[(asc_idx_local + 11) % 12]
destroyer_lords = {SIGN_LORDS_MAP.get(h8_sign, ''), SIGN_LORDS_MAP.get(h12_sign, '')}
for dl in destroyer_lords:
if dl and dl in p7:
dl_status = p7[dl].get('status', '')
if 'Exalted' in dl_status or 'Own' in dl_status:
conflicts.append({
'rule': 'Destroyer+Exalted',
'planets': [dl],
'verdict': '带毒高价值资产',
'instruction': f"{dl}既是清理者(掌8/12宫)又入旺/入庙,力量极强但方向凶险——禁止说逢凶化吉",
})
# 规则2: P5凶宫+BAV高 → 检查6/8/12宫的SAV是否 >28
asht_data = report.get('ashtakavarga', {})
house_scores = {}
if asht_data:
# 从ashtakavarga原始数据获取house_scores
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
asht_result = calc_ashtakavarga(planets, asc_idx_local)
house_scores = asht_result.get('house_scores', {})
except:
pass
for h in [6, 8, 12]:
hs = house_scores.get(f'house_{h}', {})
sav_score = hs.get('score', 0)
if sav_score > 28:
conflicts.append({
'rule': 'Dusthana+HighBAV',
'house': h,
'sav': sav_score,
'verdict': '乱世出英雄',
'instruction': f"{h}宫是凶宫但SAV={sav_score}>28),在困境中反而能出成就",
})
# 规则3: P1吉+P2受损 → 上升主星状态好但太阳(健康指标)受损
sun_info = p2.get('sun_status', '')
lord_info = p7.get(asc_lord, {})
lord_status = lord_info.get('status', '')
if ('Exalted' in lord_status or 'Own' in lord_status) and ('Debilitated' in sun_info or 'Enemy' in sun_info):
conflicts.append({
'rule': 'GoodP1+DamagedP2',
'planets': [asc_lord, 'Sun'],
'verdict': '空有雄心无着力点',
'instruction': f"上升主{asc_lord}强健但太阳受损,有野心但执行力/健康跟不上",
})
return conflicts
def cmd_audit(args):
"""P1-P12 行星审计:调用 chart→shadbala→ashtakavarga→yoga,输出统一审计报告"""
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
planets = chart.get('planets', {})
asc_sign = chart.get('ascendant', {}).get('sign', 'Unknown')
report = {
'version': '3.5',
'birth_info': chart.get('birth_info', {}),
'ascendant': chart.get('ascendant', {}),
'planets': planets,
'audit': {},
}
# P1 Identity(本命身份): 上升星座 + 上升主星
asc_lord = chart.get('ascendant', {}).get('lord', 'Unknown')
lord_info = planets.get(asc_lord, {})
report['audit']['P1_identity'] = {
'asc_sign': asc_sign,
'asc_lord': asc_lord,
'lord_sign': lord_info.get('sign', 'Unknown'),
'lord_house': lord_info.get('house', 0),
'lord_status': lord_info.get('status', '未知'),
}
# P2 Health(健康指标): 6宫 + 8宫 + 12宫主星 + 太阳状态
health_houses = [6, 8, 12]
health_lords = set()
health_info = {}
for h in health_houses:
sign_idx = (asc_idx + h - 1) % 12
sign_name = SIGNS[sign_idx]
lord = SIGN_LORDS.get(sign_name, 'Unknown')
health_lords.add(lord)
health_info[f'house_{h}'] = {'sign': sign_name, 'lord': lord}
sun_info = planets.get('Sun', {})
report['audit']['P2_health'] = {
'houses': health_info,
'sun_status': sun_info.get('status', '未知'),
'sun_house': sun_info.get('house', 0),
}
# P3 Warehouse Coupling(仓库耦合): 双宫掌管=货物捆绑
# CNWU16逻辑:如果一颗行星同时掌管两个宫位,则两个宫位的事务被"捆绑"
house_lord_map = {}
for h in range(1, 13):
sign_idx = (asc_idx + h - 1) % 12
sname = SIGNS[sign_idx]
lord = SIGN_LORDS.get(sname, 'Unknown')
if lord not in house_lord_map:
house_lord_map[lord] = []
house_lord_map[lord].append(h)
warehouse_coupling = {}
for lord, houses in house_lord_map.items():
if len(houses) > 1:
warehouse_coupling[lord] = {
'houses': houses,
'meaning': f"{lord}同时掌管{houses[0]}宫和{houses[1]}宫,事务捆绑",
'conjunction_quality': _assess_conjunction_quality(lord, houses, planets),
}
report['audit']['P3_warehouse_coupling'] = warehouse_coupling
# P8 Age Status(年龄状态): 青壮=主动, 老婴=辅助, 死=自动执行
# 基于行星在星座中的度数区间判定生命周期
age_status_map = {}
for pname, pd in planets.items():
deg_in_sign = pd.get('degree_in_sign', 0)
if pname in ['Rahu', 'Ketu']:
age_status_map[pname] = {'status': '永远逆行', 'phase': 'Rahu/Ketu无年龄状态'}
continue
if deg_in_sign < 10:
phase = '婴幼(0-10°)'
quality = '辅助型 — 能量尚未完全展开'
elif deg_in_sign < 20:
phase = '青壮(10-20°)'
quality = '主动型 — 能量最活跃,主导性强'
else:
phase = '老年(20-30°)'
quality = '自动执行型 — 已内化的能力,自动化运作'
age_status_map[pname] = {
'degree_in_sign': round(deg_in_sign, 2),
'phase': phase,
'quality': quality,
}
report['audit']['P8_age_status'] = age_status_map
# P4 Resource SAV(资源SAV& P6 Exit SAV(退出SAV
# 需要 Ashtakavarga 数据
asht_data = None
try:
sys.path.insert(0, SCRIPT_DIR)
from ashtakavarga import calc_ashtakavarga
asht_data = calc_ashtakavarga(planets, asc_idx)
report['ashtakavarga'] = {
'sav_total': asht_data.get('sav', {}).get('total', 0),
'sav_valid': asht_data.get('sav', {}).get('valid', False),
'strongest': asht_data.get('strongest_signs', []),
'weakest': asht_data.get('weakest_signs', []),
}
house_scores = asht_data.get('house_scores', {})
# P4: 财富宫(2,11) SAV
p4_info = {}
for h in [2, 11]:
hs = house_scores.get(f'house_{h}', {})
p4_info[f'house_{h}'] = hs
report['audit']['P4_resource_sav'] = p4_info
# P6: 退出宫(12) SAV + 8宫
p6_info = {}
for h in [8, 12]:
hs = house_scores.get(f'house_{h}', {})
p6_info[f'house_{h}'] = hs
report['audit']['P6_exit_sav'] = p6_info
# P5: 路况(1,5,9三宫) SAV
p5_info = {}
for h in [1, 5, 9]:
hs = house_scores.get(f'house_{h}', {})
p5_info[f'house_{h}'] = hs
report['audit']['P5_road_condition'] = p5_info
except Exception as e:
report['audit']['ashtakavarga_error'] = str(e)
# P7 Dignity(尊严状态)
dignity_map = {}
for pname, pd in planets.items():
dignity_map[pname] = {
'sign': pd.get('sign', ''),
'status': pd.get('status', '中性'),
'house': pd.get('house', 0),
'retrograde': pd.get('retrograde', False),
}
report['audit']['P7_dignity'] = dignity_map
# P9 Shadbala(六重力量)
try:
sys.path.insert(0, SCRIPT_DIR)
from shadbala import build_shadbala_context, calc_shadbala
birth_hour = _birth_hour_decimal(args.hour, args.minute, _arg_second(args))
sun_lon = planets.get('Sun', {}).get('degree', 0)
moon_lon = planets.get('Moon', {}).get('degree', 0)
context = build_shadbala_context(jd, args.lat, args.lon, _current_ayanamsa_name(args), args.tz)
shadbala = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon, context=context)
report['audit']['P9_shadbala'] = {
'summary': shadbala.get('summary', {}),
'ishta_bala_ranking': shadbala.get('ishta_bala_ranking', []),
}
except Exception as e:
report['audit']['P9_shadbala_error'] = str(e)
# P10 Aspects(相位)简化版
aspect_map = {}
for pname, pd in planets.items():
if pname in ['Rahu', 'Ketu']:
continue
house = pd.get('house', 0)
# 标准 7/4 相位(从该行星宫位数起)
aspects_from_house = {1: [7], 2: [7], 3: [5, 9, 7], 4: [7, 10],
5: [7], 6: [7], 7: [7], 8: [7],
9: [5, 7], 10: [7], 11: [7], 12: [7]}
# 特殊相位
special = {'Mars': [4, 7, 8], 'Jupiter': [5, 7, 9], 'Saturn': [3, 7, 10]}
if pname in special:
aspect_houses = special[pname]
else:
aspect_houses = [7] # 标准对宫
aspect_map[pname] = {
'from_house': house,
'aspect_houses': aspect_houses,
}
report['audit']['P10_aspects'] = aspect_map
# P11 Nakshatra
nak_map = {}
for pname, pd in planets.items():
nak_map[pname] = {
'nakshatra': pd.get('nakshatra', ''),
'pada': pd.get('nakshatra_pada', 0),
'lord': pd.get('nakshatra_lord', ''),
}
report['audit']['P11_nakshatra'] = nak_map
# P12 Yogas(格局识别)
try:
yoga_planets = {}
for pname, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd and 'house' in pd:
yoga_planets[pname] = {
'sign': pd['sign'],
'house': pd['house'],
'degree': pd.get('degree_in_sign', pd.get('degree')),
}
yoga_result = cmd_yoga(type('Args', (), {
'ascendant': asc_sign,
'planets': ','.join([
f"{k}:{v['sign']}:{v['house']}" + (f":{v['degree']}" if v.get('degree') is not None else "")
for k, v in yoga_planets.items()
])
})())
report['audit']['P12_yogas'] = {
'count': yoga_result.get('yogas_detected', 0),
'yogas': yoga_result.get('yogas', []),
}
except Exception as e:
report['audit']['P12_yogas_error'] = str(e)
# 验证
try:
from validate import validate_chart
validation = validate_chart(chart, asht_data)
report['validation'] = validation
except Exception as e:
report['validation'] = {"error": str(e)}
# 冲突仲裁(CNWU16框架3条规则)
report['conflict_arbitration'] = _conflict_arbitration(report)
return report
# ============================================================================
# 14. 报告生成(v3.6新增)
# ============================================================================
def cmd_report(args):
"""调用 report_builder.py 生成 HTML 报告"""
try:
sys.path.insert(0, SCRIPT_DIR)
from report_builder import main as report_main
except ImportError as e:
return {"error": f"report_builder模块导入失败: {e}"}
# 构造 sys.argv 并调用 report_builder
folder = args.folder
if not os.path.isdir(folder):
return {"error": f"目录不存在: {folder}"}
report_argv = [
'report_builder.py', folder,
'--name', args.name,
'--lagna', args.lagna,
'--gender', args.gender,
'--status', args.status,
'--lang', args.lang,
]
if args.output:
report_argv.extend(['--output', args.output])
old_argv = sys.argv
sys.argv = report_argv
try:
report_main()
output_path = args.output or os.path.join(folder, 'report.html')
return {
'status': 'ok',
'output': output_path,
'name': args.name,
'lagna': args.lagna,
'lang': args.lang,
}
except SystemExit:
return {"error": "report_builder执行出错,请检查MD文件格式"}
except Exception as e:
return {"error": f"报告生成失败: {e}"}
finally:
sys.argv = old_argv
# ============================================================================
# 15. BPHS十六分盘完整计算(v3.7新增)
# ============================================================================
def cmd_varga_full(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
# --- Custom D-N mode (v6.9.12) ---
custom_n = getattr(args, 'custom', None)
if custom_n:
try:
sys.path.insert(0, SCRIPT_DIR)
from divisional_charts_extended import DivisionalChartsCalculator
calc = DivisionalChartsCalculator()
except ImportError as e:
return {"error": f"divisional_charts_extended模块导入失败: {e}"}
planets = chart.get('planets', {})
planet_lons = {pn: pd.get('degree_raw', pd['degree']) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
result = {'custom_div': custom_n}
result['Ascendant'] = calc.calc_custom_varga(asc_deg, custom_n)
for pn, lon in planet_lons.items():
result[pn] = calc.calc_custom_varga(lon, custom_n)
return result
# --- Composite D-m×n mode (v6.9.12) ---
composite = getattr(args, 'composite', None)
if composite:
try:
sys.path.insert(0, SCRIPT_DIR)
from divisional_charts_extended import DivisionalChartsCalculator
calc = DivisionalChartsCalculator()
except ImportError as e:
return {"error": f"divisional_charts_extended模块导入失败: {e}"}
parts = [int(x.strip()) for x in composite.split(',')]
if len(parts) != 2:
return {"error": "--composite 需要两个整数,逗号分隔(如 9,12 表示D9×D12)"}
outer, inner = parts
planets = chart.get('planets', {})
planet_lons = {pn: pd.get('degree_raw', pd['degree']) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
result = {'composite_div': f'D{outer}×D{inner}=D{outer*inner}', 'outer': outer, 'inner': inner}
result['Ascendant'] = calc.calc_composite_varga(asc_deg, outer, inner)
for pn, lon in planet_lons.items():
result[pn] = calc.calc_composite_varga(lon, outer, inner)
return result
# --- Standard / variant mode ---
variant = getattr(args, 'variant', None)
try:
sys.path.insert(0, SCRIPT_DIR)
from varga import calc_all_vargas
except ImportError as e:
return {"error": f"varga模块导入失败: {e}"}
planets = chart.get('planets', {})
planet_lons = {pn: pd.get('degree_raw', pd['degree']) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
divisions = [int(d.strip().replace('D','')) for d in args.divisions.split(',')] if args.divisions else None
# If variant requested for D2/D3, use DivisionalChartsCalculator
if variant and divisions and len(divisions) == 1 and divisions[0] in (2, 3):
try:
sys.path.insert(0, SCRIPT_DIR)
from divisional_charts_extended import DivisionalChartsCalculator
calc = DivisionalChartsCalculator()
except ImportError:
variant = None # fallback to standard
if variant:
result = {'variant': variant, 'div': divisions[0]}
result['Ascendant'] = calc.calc_varga_with_variant(asc_deg, divisions[0], variant)
for pn, lon in planet_lons.items():
result[pn] = calc.calc_varga_with_variant(lon, divisions[0], variant)
return result
try:
sys.path.insert(0, SCRIPT_DIR)
from divisional_charts_extended import DivisionalChartsCalculator, VargaType
extended_available = {varga.division: varga for varga in VargaType}
if any(division not in {2, 3, 4, 7, 9, 10, 12, 16, 20, 24, 27, 30, 40, 45, 60} for division in (divisions or [])):
calc = DivisionalChartsCalculator()
selected = [extended_available[division] for division in divisions]
result = {}
for varga in selected:
key = f'D{varga.division}_{varga.varga_name}'
result[key] = calc._calculate_single_varga(varga, planet_lons, asc_deg)
return result
except KeyError as e:
return {"error": f"不支持的D{e.args[0]}。请使用 --custom N 计算任意D-N分盘。"}
except ImportError:
pass
return calc_all_vargas(planet_lons, asc_deg, divisions)
# ============================================================================
# 16. 度数精确相位系统(v3.7新增)
# ============================================================================
def cmd_aspects(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from aspects import calc_all_aspects
except ImportError as e:
return {"error": f"aspects模块导入失败: {e}"}
planets = chart.get('planets', {})
planet_lons = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'degree' in pd:
planet_lons[pn] = pd['degree']
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
return calc_all_aspects(planet_lons, asc_deg)
# ============================================================================
# 17. Jaimini系统(v3.7新增)
# ============================================================================
def cmd_jaimini(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from jaimini import (calc_chara_karaka_7, calc_chara_karaka_8, calc_chara_dasha,
calc_karakamsha, calc_chara_dasha_with_antardasha,
calc_arudha_padas, calc_graha_padas, calc_special_lagnas)
from varga import calc_varga
except ImportError as e:
return {"error": f"jaimini模块导入失败: {e}"}
planets = chart.get('planets', {})
planet_lons = {}
planet_degs = {} # 星座内度数(0-30),用于Jaimini Karaka计算
for pn, pd in planets.items():
if isinstance(pd, dict) and 'degree' in pd:
planet_lons[pn] = pd['degree']
planet_degs[pn] = pd.get('degree_in_sign', pd['degree'] % 30)
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
result = {}
# Chara Karaka(必须传星座内度数0-30,不是完整经度0-360)
mode = args.mode or 'all'
if mode in ('all', 'karaka'):
result['chara_karaka_7'] = calc_chara_karaka_7(planet_degs)
result['chara_karaka_8'] = calc_chara_karaka_8(planet_degs)
if mode in ('all', 'dasha'):
use_antardasha = getattr(args, 'antardasha', False)
if use_antardasha:
result['chara_dasha'] = calc_chara_dasha_with_antardasha(asc_idx, planet_lons, args.year, args.month)
else:
result['chara_dasha'] = calc_chara_dasha(asc_idx, planet_lons, args.year, args.month)
if mode in ('all', 'karakamsha'):
# AK(灵魂星)的D9位置 — Karakamsha定义是AK在Navamsa中的星座
# ⚠️ 2026-05-03修正:此前错误使用DK,现已修正为AK
ck7 = calc_chara_karaka_7(planet_degs)
ak_name = ck7['karaka_table']['Atmakaraka']['planet']
ak_lon = planet_lons.get(ak_name, 0)
ak_d9 = calc_varga(ak_lon, 9)
result['karakamsha'] = calc_karakamsha(ak_d9.get('sign', 'Aries'), ak_d9.get('degree_in_sign', 0))
if mode in ('all', 'arudha'):
result['arudha_padas'] = calc_arudha_padas(asc_idx, planet_lons)
result['graha_padas'] = calc_graha_padas(planet_lons)
if mode in ('all', 'special'):
result['special_lagnas'] = calc_special_lagnas(asc_idx, args.hour, args.minute + _arg_second(args) / 60.0)
return result
# ============================================================================
# 18. 高级Nakshatra分析(v3.7 → v6.0.22 移至 cmd_nakshatra_adv.py
# ============================================================================
def cmd_narayana_dasha(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is not None:
chart['ascendant_index'] = asc_idx
return _cmd_narayana_dasha_impl(args, chart)
def cmd_nakshatra_adv(args):
from cmd_nakshatra_adv import cmd_nakshatra_adv as _impl
return _impl(args)
def cmd_nakshatra_dasha(args):
from cmd_nakshatra_adv import cmd_nakshatra_dasha as _impl
return _impl(args)
def cmd_nakshatra_full(args):
from cmd_nakshatra_adv import cmd_nakshatra_full as _impl
return _impl(args)
# ============================================================================
# 19. Argala门闩系统(v3.7新增)
# ============================================================================
def cmd_argala(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from argala import calc_argala
except ImportError as e:
return {"error": f"argala模块导入失败: {e}"}
planets = chart.get('planets', {})
# 构建宫位到行星的映射 - argala需要sign_indices
planet_sign_indices = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd:
si = SIGNS.index(pd['sign']) if pd['sign'] in SIGNS else 0
planet_sign_indices[pn] = si
return calc_argala(planet_sign_indices, asc_idx)
# ============================================================================
# 20. Tajika/Varshaphala年运盘(v3.7新增)
# ============================================================================
def cmd_tajika(args):
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from tajika import calc_muntha, calc_year_lord, calc_mudda_dasha, calc_tri_pataka
except ImportError as e:
return {"error": f"tajika模块导入失败: {e}"}
planets = chart.get('planets', {})
planet_lons = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'degree' in pd:
planet_lons[pn] = pd['degree']
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
asc_si = int(asc_deg / 30) % 12 # sign index
age = args.age
if age is None:
return {"error": "请提供 --age 参数(当前年龄)"}
result = {}
mode = args.mode or 'all'
if mode in ('all', 'muntha'):
result['muntha'] = calc_muntha(asc_si, age)
if mode in ('all', 'yearlord'):
result['year_lord'] = calc_year_lord(asc_si, age)
if mode in ('all', 'mudda'):
# 需要先获取 varsha_lord
yl = calc_year_lord(asc_si, age)
varsha_lord = yl.get('year_lord', 'Jupiter')
result['mudda_dasha'] = calc_mudda_dasha(asc_si, varsha_lord, args.month)
if mode in ('all', 'tripataka'):
yl = calc_year_lord(asc_si, age)
varsha_lord = yl.get('year_lord', 'Jupiter')
muntha_si = (asc_si + age) % 12
result['tri_pataka'] = calc_tri_pataka(planet_lons, varsha_lord, muntha_si)
return result
# ============================================================================
# 21. 合盘分析(v3.7新增)
# ============================================================================
def cmd_synastry(args):
try:
sys.path.insert(0, SCRIPT_DIR)
from synastry import calc_synastry
except ImportError as e:
return {"error": f"synastry模块导入失败: {e}"}
# 构建两人数据
p1 = {'moon_lon': args.moon1, 'gender': args.gender1 or 'M'}
p2 = {'moon_lon': args.moon2, 'gender': args.gender2 or 'F'}
if args.mars1 is not None: p1['mars_lon'] = args.mars1
if args.mars2 is not None: p2['mars_lon'] = args.mars2
if args.asc1 is not None: p1['asc_lon'] = args.asc1
if args.asc2 is not None: p2['asc_lon'] = args.asc2
return calc_synastry(p1, p2)
# ============================================================================
# 22. 全自动综合解盘 full-readingv3.7.1新增)
# ============================================================================
# ============================================================================
# Transit 多参考点分析(v4.5.0 P1补齐)
# 基于 transit-multi-reference-guide.md 强制规范
# 四参考点:Lagna / Chandra Lagna / Arudha Lagna / Navamsa Lagna
# ============================================================================
def _calc_transit_multi_reference(planets, asc_idx, asc_deg, planet_lons, transit_planets=None, transit_date=None, node_mode='mean'):
"""
⭐ v4.5.0: Transit多参考点分析(强制规范)
每次Transit分析必须同时从四个参考点评估:
1. Lagna(上升点)— 实际生活事件
2. Chandra Lagna(月亮星座)— 心理状态、职业变动
3. Arudha LagnaAL)— 公众形象
4. Navamsa LagnaD9上升)— 灵魂层面
"""
data_layer = 'true_transit_positions' if transit_planets else 'natal_positions_fallback'
analysis_planets = transit_planets if transit_planets else planets
# 四个参考点的星座索引
moon_lon = planet_lons.get('Moon', 0)
chandra_idx = int(moon_lon / 30) % 12
# Arudha Lagna(从special_lagnas模块逻辑简化:AL = (Ascendant度数+12宫主度数)%360 对应的星座)
twelfth_sign_idx = (asc_idx + 11) % 12
twelfth_lord = SIGN_LORDS.get(SIGNS[twelfth_sign_idx], '')
twelfth_lord_lon = planet_lons.get(twelfth_lord, 0)
al_raw = (asc_deg + twelfth_lord_lon) % 360
# AL 特殊规则:如果结果落在原始宫或第7宫,取对宫
al_idx = int(al_raw / 30) % 12
if al_idx == asc_idx or al_idx == (asc_idx + 6) % 12:
al_idx = (al_idx + 7) % 12 # 取第8个 = 对宫再移一位
# Navamsa Lagna
d9_asc_idx = _navamsa_idx(asc_deg)
references = {
'Lagna': {
'name': 'Lagna',
'cn': '上升点',
'sign': SIGNS[asc_idx],
'sign_cn': SIGNS_CN[SIGNS[asc_idx]],
'sign_idx': asc_idx,
'priority': 'P1',
'scope': '实际生活事件、身体健康',
},
'Chandra_Lagna': {
'name': 'Chandra Lagna',
'cn': '月亮上升',
'sign': SIGNS[chandra_idx],
'sign_cn': SIGNS_CN[SIGNS[chandra_idx]],
'sign_idx': chandra_idx,
'priority': 'P1',
'scope': '心理状态、职业变动、情感体验',
},
'Arudha_Lagna': {
'name': 'Arudha Lagna',
'cn': '形象上升',
'sign': SIGNS[al_idx],
'sign_cn': SIGNS_CN[SIGNS[al_idx]],
'sign_idx': al_idx,
'priority': 'P2',
'scope': '公众形象、社会认知、他人如何看待你',
},
'Navamsa_Lagna': {
'name': 'Navamsa Lagna',
'cn': '灵性上升',
'sign': SIGNS[d9_asc_idx],
'sign_cn': SIGNS_CN[SIGNS[d9_asc_idx]],
'sign_idx': d9_asc_idx,
'priority': 'P3',
'scope': '灵魂层面的实际影响、内在真实',
},
}
# 对每个外行星(Jupiter/Saturn/Rahu/Ketu),计算从四个参考点看的宫位
OUTER_PLANETS = ['Jupiter', 'Saturn', 'Rahu', 'Ketu']
transit_analysis = {}
for pn in OUTER_PLANETS:
pd = analysis_planets.get(pn, {})
if not isinstance(pd, dict) or 'sign' not in pd:
continue
p_sign_idx = SIGNS.index(pd['sign']) if pd['sign'] in SIGNS else 0
transit_analysis[pn] = {
'sign': pd['sign'],
'sign_cn': SIGNS_CN.get(pd['sign'], ''),
'degree_in_sign': pd.get('degree_in_sign', pd.get('degree', 0) % 30),
'house_from_ref': {},
}
for ref_name, ref_info in references.items():
ref_idx = ref_info['sign_idx']
house = ((p_sign_idx - ref_idx) % 12) + 1
house_meaning = _house_theme(house)
transit_analysis[pn]['house_from_ref'][ref_name] = {
'house': house,
'meaning': house_meaning,
}
# 差异检测:同一行星在不同参考点的宫位含义是否矛盾
divergences = []
for pn, pa in transit_analysis.items():
houses = {ref: info['house'] for ref, info in pa['house_from_ref'].items()}
unique_houses = set(houses.values())
if len(unique_houses) > 1:
divergences.append({
'planet': pn,
'houses': houses,
'divergence': f'{pn}在四个参考点分别落在不同宫位,需综合判断',
})
# Sade Sati / Ashtama Shani 检测(基于Chandra Lagna
special_checks = {}
saturn_sign_idx = SIGNS.index(analysis_planets.get('Saturn', {}).get('sign', 'Aries')) if isinstance(analysis_planets.get('Saturn'), dict) and analysis_planets.get('Saturn', {}).get('sign') in SIGNS else 0
# Sade Sati: Saturn 在月亮星座或前后1宫
sade_sati_phase = None
if saturn_sign_idx == chandra_idx:
sade_sati_phase = 'peak'
elif saturn_sign_idx == (chandra_idx - 1) % 12:
sade_sati_phase = 'rising'
elif saturn_sign_idx == (chandra_idx + 1) % 12:
sade_sati_phase = 'setting'
if sade_sati_phase:
special_checks['sade_sati'] = {
'active': True,
'phase': sade_sati_phase,
'note': f'土星过境月亮{SIGNS_CN[SIGNS[chandra_idx]]}附近,Sade Sati {sade_sati_phase}',
}
# Ashtama Shani: Saturn在月亮第8宫
saturn_from_chandra = ((saturn_sign_idx - chandra_idx) % 12) + 1
if saturn_from_chandra == 8:
special_checks['ashtama_shani'] = {
'active': True,
'note': '土星过境月亮第8宫(Ashtama Shani),压力期',
}
return {
'references': references,
'target_date': transit_date,
'node_mode': node_mode,
'data_layer': data_layer,
'transit_analysis': transit_analysis,
'divergences': divergences,
'divergence_count': len(divergences),
'special_checks': special_checks,
'protocol': 'v6.0.10 Transit多参考点强制规范:AI必须用真实过境行星位置,并同时呈现Lagna和Chandra Lagna两个视角。任何矛盾信号必须记录并解释。',
}
# ============================================================================
# Dasa Convergence 多系统交叉验证(v6.1.6
# 基于 dasa-convergence-methodology.md
# 系统:Vimshottari + Chara Dasha + Yogini + Ashtottari + Kalachakra
# ============================================================================
# Yogini Dasha 常量
YOGINI_ORDER = ['Mangala', 'Pingala', 'Dhanya', 'Bhramari', 'Bhadrika', 'Ulka', 'Siddha', 'Sankata']
YOGINI_YEARS = {'Mangala': 1, 'Pingala': 2, 'Dhanya': 3, 'Bhramari': 4, 'Bhadrika': 5, 'Ulka': 6, 'Siddha': 7, 'Sankata': 8}
# Yogini 从月亮 Nakshatra 的第3个 NakshatraDhanishta)开始计数
YOGINI_NAK_START = 23 # Dhanishta 在 NAKSHATRA_LIST 中的索引
def _calc_yogini_dasha(moon_lon, birthdate_str):
"""
计算 Yogini Dasha 时间线
Yogini 基于 8 位女神循环,总周期 36 年
起始点由月亮所在 Nakshatra 决定
"""
nak_idx = int(moon_lon / (360 / 27)) % 27
# Yogini 起始索引 = (nak_idx - YOGINI_NAK_START) % 8
yog_start = (nak_idx - YOGINI_NAK_START) % 8
# 余数 = 在当前 Yogini 周期中的已过比例
nak_in_yog = nak_idx % 8 # 在8分组的第几个
pada = int((moon_lon % (360/27)) / (360/108)) + 1
# 余数比例
balance_frac = (nak_in_yog * 4 + pada - 1) / 32 # 8 Nakshatra × 4 Pada = 32 份
balance_frac = min(balance_frac, 1.0)
birth_date = datetime.strptime(birthdate_str, '%Y-%m-%d')
maha_periods = []
total_years = 0
for i in range(8):
idx = (yog_start + i) % 8
name = YOGINI_ORDER[idx]
years = YOGINI_YEARS[name]
if i == 0:
elapsed_years = years * balance_frac
actual_years = years - elapsed_years
start_offset = total_years
maha_periods.append({
'yogini': name,
'full_years': years,
'balance_years': round(actual_years, 3),
'start_offset_years': round(start_offset, 3),
'start_date': (birth_date + timedelta(days=round(start_offset * 365.25))).strftime('%Y-%m-%d'),
'end_date': (birth_date + timedelta(days=round((start_offset + actual_years) * 365.25))).strftime('%Y-%m-%d'),
'is_current_start': True,
})
total_years += actual_years
else:
start_offset = total_years
maha_periods.append({
'yogini': name,
'full_years': years,
'start_offset_years': round(start_offset, 3),
'start_date': (birth_date + timedelta(days=round(start_offset * 365.25))).strftime('%Y-%m-%d'),
'end_date': (birth_date + timedelta(days=round((start_offset + years) * 365.25))).strftime('%Y-%m-%d'),
})
total_years += years
# 计算当前 Yogini
today = datetime.now()
age_days = (today - birth_date).days
age_years = age_days / 365.25
cycle_years = 36 # Yogini 总周期
current_in_cycle = age_years % cycle_years
current_yogini = None
cumulative = 0
for yp in maha_periods:
dur = yp.get('balance_years', yp['full_years'])
if cumulative <= current_in_cycle < cumulative + dur:
current_yogini = yp
break
cumulative += dur
return {
'moon_nakshatra_idx': nak_idx,
'yogini_start_index': yog_start,
'total_cycle_years': 36,
'maha_periods': maha_periods,
'current_yogini': current_yogini,
}
def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, planet_lons, asc_idx, ashtottari_result=None, kalachakra_result=None):
"""
⭐ v6.1.6: Dasa Convergence 多系统交叉验证
Vimshottari + Chara Dasha + Yogini + Ashtottari + Kalachakra 同时激活同一生活领域时,概率大幅提升。
Chara Dasha 计算层为 covered;事件预测仍需多系统交叉确认。
"""
# 提取各系统当前周期
convergence_data = {'systems': {}}
# 系统1: Vimshottari
if isinstance(dasha_result, dict):
current_d = dasha_result.get('current_dasha', {})
if isinstance(current_d, dict):
maha = current_d.get('mahadasha', current_d.get('maha'))
antar = current_d.get('antardasha', current_d.get('antar'))
convergence_data['systems']['vimshottari'] = {
'maha': maha,
'antar': antar,
'pratyantar': current_d.get('pratyantar'),
'basis': 'Nakshatra (Moon)',
}
# 系统2: Chara Dasha
if isinstance(chara_dasha_result, dict):
cd_maha = chara_dasha_result.get('current_maha', chara_dasha_result.get('current'))
cd_antar = chara_dasha_result.get('current_antar')
if isinstance(cd_maha, dict):
cd_sign = cd_maha.get('sign', cd_maha.get('rashi'))
elif isinstance(cd_maha, str):
cd_sign = cd_maha
else:
cd_sign = None
convergence_data['systems']['chara_dasha'] = {
'maha_sign': cd_sign,
'antar_sign': cd_antar.get('sign', cd_antar) if isinstance(cd_antar, dict) else cd_antar,
'basis': 'Rashi (Sign-based)',
}
# 系统3: Yogini
if isinstance(yogini_result, dict):
cur_yog = yogini_result.get('current_yogini') or yogini_result.get('current') or {}
convergence_data['systems']['yogini'] = {
'yogini': cur_yog.get('yogini') if isinstance(cur_yog, dict) else None,
'planet': cur_yog.get('planet') if isinstance(cur_yog, dict) else None,
'years': cur_yog.get('full_years', cur_yog.get('years')) if isinstance(cur_yog, dict) else None,
'basis': 'Nakshatra/Lagna 36-year cycle',
}
# 系统4: Ashtottari Dasha(条件性)
if isinstance(ashtottari_result, dict):
cur_ash = ashtottari_result.get('current') or {}
convergence_data['systems']['ashtottari'] = {
'applicable': ashtottari_result.get('applicable', True),
'planet': cur_ash.get('planet') if isinstance(cur_ash, dict) else None,
'years': cur_ash.get('years') if isinstance(cur_ash, dict) else None,
'basis': 'Conditional Nakshatra/Paksha 108-year cycle',
}
# 系统5: Kalachakra Dasha
if isinstance(kalachakra_result, dict):
cur_kal = kalachakra_result.get('current') or {}
convergence_data['systems']['kalachakra'] = {
'mode': kalachakra_result.get('mode'),
'lord': cur_kal.get('lord') if isinstance(cur_kal, dict) else None,
'rashi': cur_kal.get('rashi') if isinstance(cur_kal, dict) else None,
'years': cur_kal.get('years') if isinstance(cur_kal, dict) else None,
'basis': 'Moon Nakshatra Pada / Rashi-year cycle',
}
# 宫位主题映射
house_themes_map = {
1: 'self_health', 2: 'wealth_family', 3: 'communication_skill', 4: 'home_mother',
5: 'creativity_children', 6: 'health_service', 7: 'marriage_partnership',
8: 'transformation', 9: 'fortune_dharma', 10: 'career_status',
11: 'gains_wishes', 12: 'loss_spirituality',
}
# 逐领域检测三系统激活
domain_activations = {}
for house, domain in house_themes_map.items():
activations = []
# Vimshottari: 检查大运/小运行星是否关联该宫
vims = convergence_data['systems'].get('vimshottari', {})
if vims:
for level in ['maha', 'antar']:
planet = vims.get(level)
if planet and isinstance(planet, str):
# 该行星是否掌管此宫?
target_sign_idx = (asc_idx + house - 1) % 12
target_sign = SIGNS[target_sign_idx]
target_lord = SIGN_LORDS.get(target_sign, '')
if planet == target_lord:
activations.append({
'system': 'Vimshottari',
'level': level,
'planet': planet,
'reason': f'{planet}{house}宫({target_sign})的宫主星',
})
# 该行星是否落在此宫?
p_sign_idx = int(planet_lons.get(planet, 0) / 30) % 12
p_house = ((p_sign_idx - asc_idx) % 12) + 1
if p_house == house:
activations.append({
'system': 'Vimshottari',
'level': level,
'planet': planet,
'reason': f'{planet}落在{house}',
})
# Chara Dasha: 检查当前星座是否关联该宫
cd = convergence_data['systems'].get('chara_dasha', {})
if cd and cd.get('maha_sign'):
cd_sign = cd['maha_sign']
if cd_sign in SIGNS:
cd_sign_idx = SIGNS.index(cd_sign)
cd_house_from_asc = ((cd_sign_idx - asc_idx) % 12) + 1
if cd_house_from_asc == house:
activations.append({
'system': 'Chara Dasha',
'level': 'maha',
'sign': cd_sign,
'reason': f'Chara大运星座{cd_sign}{house}',
})
# Yogini / Ashtottari: 当前行星是否掌管或落入该宫
for system_key, system_label, planet_key in [
('yogini', 'Yogini', 'planet'),
('ashtottari', 'Ashtottari', 'planet'),
]:
sys_data = convergence_data['systems'].get(system_key, {})
planet = sys_data.get(planet_key)
if not planet or not isinstance(planet, str):
continue
target_sign_idx = (asc_idx + house - 1) % 12
target_sign = SIGNS[target_sign_idx]
target_lord = SIGN_LORDS.get(target_sign, '')
if planet == target_lord:
activations.append({
'system': system_label,
'level': 'maha',
'planet': planet,
'reason': f'{system_label}当前主星{planet}{house}宫({target_sign})的宫主星',
})
if planet in planet_lons:
p_sign_idx = int(planet_lons.get(planet, 0) / 30) % 12
p_house = ((p_sign_idx - asc_idx) % 12) + 1
if p_house == house:
activations.append({
'system': system_label,
'level': 'maha',
'planet': planet,
'reason': f'{system_label}当前主星{planet}落在{house}',
})
# Kalachakra: 当前 Rashi 是否关联该宫,当前 lord 是否掌管或落入该宫
kal = convergence_data['systems'].get('kalachakra', {})
if kal:
kal_rashi = kal.get('rashi')
if kal_rashi in SIGNS:
kal_sign_idx = SIGNS.index(kal_rashi)
kal_house_from_asc = ((kal_sign_idx - asc_idx) % 12) + 1
if kal_house_from_asc == house:
activations.append({
'system': 'Kalachakra',
'level': 'maha_rashi',
'sign': kal_rashi,
'reason': f'Kalachakra当前推运星座{kal_rashi}{house}',
})
kal_lord = kal.get('lord')
if kal_lord and isinstance(kal_lord, str):
target_sign_idx = (asc_idx + house - 1) % 12
target_sign = SIGNS[target_sign_idx]
target_lord = SIGN_LORDS.get(target_sign, '')
if kal_lord == target_lord:
activations.append({
'system': 'Kalachakra',
'level': 'maha_lord',
'planet': kal_lord,
'reason': f'Kalachakra当前主星{kal_lord}{house}宫({target_sign})的宫主星',
})
if kal_lord in planet_lons:
p_sign_idx = int(planet_lons.get(kal_lord, 0) / 30) % 12
p_house = ((p_sign_idx - asc_idx) % 12) + 1
if p_house == house:
activations.append({
'system': 'Kalachakra',
'level': 'maha_lord',
'planet': kal_lord,
'reason': f'Kalachakra当前主星{kal_lord}落在{house}',
})
if activations:
domain_activations[domain] = {
'house': house,
'activations': activations,
'system_count': len(set(a['system'] for a in activations)),
}
# 收敛等级评估
for domain, info in domain_activations.items():
sc = info['system_count']
if sc >= 4:
info['convergence_level'] = 'L5'
info['probability'] = '85-92%'
info['interpretation'] = '四个及以上推运系统同时激活,顶级收敛信号'
elif sc >= 3:
info['convergence_level'] = 'L4'
info['probability'] = '75-85%'
info['interpretation'] = '三系统同时激活,极强信号'
elif sc >= 2:
info['convergence_level'] = 'L3'
info['probability'] = '50-65%'
info['interpretation'] = '双系统激活,强信号'
else:
info['convergence_level'] = 'L1'
info['probability'] = '+15-20%'
info['interpretation'] = '单系统激活,需Transit确认'
# 收敛窗口(最高优先级的领域)
top_domains = sorted(domain_activations.items(), key=lambda x: x[1]['system_count'], reverse=True)[:5]
return {
'systems_summary': convergence_data['systems'],
'domain_activations': domain_activations,
'top_convergent_domains': [(d, info['convergence_level']) for d, info in top_domains],
'protocol': 'v6.1.6 Dasa Convergence 多系统交叉验证。收敛等级: L1(单系统)→L3(双系统)→L4(三系统)→L5(四个及以上系统)。Chara Dasha 计算层为 covered;所有预测仍必须由 Vimshottari/Transit/Varga 等独立层确认。',
}
def _calc_actionable_context(planets, asc_idx):
"""⭐ v4.1.0: 计算Transit Actionable Output所需的上下文数据
输出:宫位激活映射 + 关键行星宫位关系 → 供AI生成Actionable Output时直接引用
"""
# 宫位主星映射(哪个行星掌管哪个宫)
house_lord_map = {}
for h in range(1, 13):
sign_idx = (asc_idx + h - 1) % 12
sname = SIGNS[sign_idx]
lord = SIGN_LORDS.get(sname, 'Unknown')
house_lord_map[h] = {'sign': sname, 'lord': lord}
# 行星落宫映射(哪个行星落在哪个宫)
planet_house_map = {}
for pn, pd in planets.items():
if isinstance(pd, dict):
planet_house_map[pn] = {
'sign': pd.get('sign', 'Unknown'),
'house': pd.get('house', 0),
'degree_in_sign': pd.get('degree_in_sign', 0),
}
# 双宫掌管检测(仓库耦合)
lord_to_houses = {}
for h, info in house_lord_map.items():
lord = info['lord']
if lord not in lord_to_houses:
lord_to_houses[lord] = []
lord_to_houses[lord].append(h)
warehouse_coupling = {l: hs for l, hs in lord_to_houses.items() if len(hs) > 1}
# 关键宫位激活条件(Transit激活该宫需要哪些行星过境相关星座)
KEY_HOUSES = {
2: '财富/家庭/语言',
3: '沟通/技能/短途/内容创作',
5: '创造力/子女/投机/恋爱',
7: '婚姻/合作/公开对手',
9: '长途旅行/高等教育/宗教/出版',
10: '事业/社会地位/权威',
11: '收入/愿望/社交网络/贵人',
12: '海外/灵性/损失/解脱',
}
activation_map = {}
for h, meaning in KEY_HOUSES.items():
sign_idx = (asc_idx + h - 1) % 12
sname = SIGNS[sign_idx]
lord = SIGN_LORDS.get(sname, 'Unknown')
lord_house = planet_house_map.get(lord, {}).get('house', 0)
activation_map[h] = {
'house_meaning': meaning,
'sign': sname,
'lord': lord,
'lord_house': lord_house,
'transit_triggers': f'{lord}过境{h}宫({sname})或{lord}本身过境相关宫位',
'double_transit_hint': f'需要Saturn和Jupiter同时激活{h}宫或{h}宫主所在宫',
}
# 内容创作相关信号(3宫/5宫/9宫/10宫交叉)
content_signals = {}
for h in [3, 5, 9, 10]:
if h in activation_map:
content_signals[f'house_{h}'] = activation_map[h]
# 贵人相关信号(7宫/9宫/11宫)
mentor_signals = {}
for h in [7, 9, 11]:
if h in activation_map:
mentor_signals[f'house_{h}'] = activation_map[h]
return {
'house_lord_map': house_lord_map,
'planet_house_map': planet_house_map,
'warehouse_coupling': warehouse_coupling,
'key_house_activations': activation_map,
'content_creation_context': content_signals,
'mentor_discovery_context': mentor_signals,
'actionable_hint': 'AI应基于此上下文生成Transit Actionable Output:每条Transit预测必须包含时间段+行动类型+置信度。详见SKILL.md Transit Actionable Output规范。',
}
def cmd_full_reading(args):
"""
用户只需提供出生信息,引擎自动串起全链路分析:
chart → dasha → yoga → varga-full → aspects → jaimini → nakshatra-adv
→ argala → tajika → shadbala → ashtakavarga → validate → audit
→ 综合报告输出
"""
import time
t0 = time.perf_counter()
stage_timings = []
profile_stages = _full_reading_profiler_enabled(args)
def _build_whole_sign_houses(asc_index, planets_data):
"""Build a compatibility house map for add-on modules.
compute_chart_data() exposes Placidus/equal-style cusp keys (house_1...),
while several v6.0.14-16 add-on modules expect 1..12 keys plus Hn_Lord.
This adapter keeps those modules wired without changing their public API.
"""
house_map = {}
for house_num in range(1, 13):
sign_idx = (asc_index + house_num - 1) % 12
sign_name = SIGNS[sign_idx]
house_map[house_num] = {
'sign': sign_name,
'lord': SIGN_LORDS.get(sign_name, ''),
'planets': [],
'strength': 'Neutral',
}
house_map[str(house_num)] = house_map[house_num]
house_map[f'H{house_num}_Lord'] = house_map[house_num]['lord']
for planet_name, planet_data in planets_data.items():
if isinstance(planet_data, dict):
house_num = planet_data.get('house')
if isinstance(house_num, int) and house_num in house_map:
house_map[house_num]['planets'].append(planet_name)
return house_map
def _varga_planet_lons(varga_chart):
"""Convert calc_all_vargas() planet sign/degree data to longitude dict."""
lons = {}
if not isinstance(varga_chart, dict):
return lons
for planet_name, planet_data in varga_chart.items():
if planet_name.startswith('_') or planet_name == 'Ascendant':
continue
if isinstance(planet_data, dict) and 'sign_idx' in planet_data:
lons[planet_name] = planet_data['sign_idx'] * 30 + planet_data.get('degree_in_sign', 0)
return lons
report = {
'version': '4.4.0-full-reading',
'birth_info': {
'date': f"{args.year}-{args.month:02d}-{args.day:02d}",
'time': _birth_time_string(args.hour, args.minute, _arg_second(args)),
'hour': int(args.hour),
'minute': int(args.minute),
'second': _arg_second(args),
'lat': args.lat, 'lon': args.lon,
'tz': f"UTC{'+' if args.tz >= 0 else ''}{args.tz}",
},
'modules': {},
'errors': [],
'warnings': [],
}
# ── Step 1: 核心星盘 ──
stage_started = time.perf_counter()
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装,无法计算星盘"}
chart_birth = chart.get('birth_info', {}) if isinstance(chart, dict) else {}
report['birth_info']['ayanamsa'] = chart_birth.get('ayanamsa')
report['birth_info']['ayanamsa_name'] = chart_birth.get('ayanamsa_name', _current_ayanamsa_name(args))
report['birth_info']['ayanamsa_display'] = chart_birth.get(
'ayanamsa_display',
_ayanamsa_display_name(report['birth_info']['ayanamsa_name']),
)
report['birth_info']['node_mode'] = chart_birth.get('node_mode', getattr(args, 'node_mode', 'mean'))
report['chart'] = chart
report['modules']['chart'] = chart
planets = chart.get('planets', {})
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
asc_sign = chart.get('ascendant', {}).get('sign', 'Unknown')
planet_lons = {pn: pd.get('degree_raw', pd['degree']) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
planet_degs = {pn: pd.get('degree_in_sign_raw', pd.get('degree_in_sign', pd['degree'] % 30)) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
houses = _build_whole_sign_houses(asc_idx, planets)
report['modules']['house_map'] = houses
planet_sign_indices = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd:
planet_sign_indices[pn] = SIGNS.index(pd['sign']) if pd['sign'] in SIGNS else 0
_record_stage_timing(
stage_timings,
'core_chart_and_setup',
stage_started,
enabled=profile_stages,
details={'modules': ['chart', 'house_map']},
)
# ── Step 1.5: Special Lagnas 特殊上升点 (v4.4.0) ──
stage_started = time.perf_counter()
try:
sys.path.insert(0, SCRIPT_DIR)
from special_lagnas import SpecialLagnasCalculator
sl_calc = SpecialLagnasCalculator()
birth_dt = _birth_datetime_from_args(args)
# 简化处理:sunrise 近似为 6:00 当地时间
sunrise_dt = datetime(args.year, args.month, args.day, 6, 0)
sl_result = sl_calc.calculate_all_lagnas(
asc_degree=asc_deg,
sun_degree=planet_lons.get('Sun', 0),
moon_degree=planet_lons.get('Moon', 0),
birth_time=birth_dt,
sunrise_time=sunrise_dt
)
# 补充 Arudha Lagna、A10/Karma Pada 和 Upapada Lagna
try:
first_house_sign_idx = asc_idx
first_lord = SIGN_LORDS.get(SIGNS[first_house_sign_idx], '')
first_lord_deg = planet_lons.get(first_lord, 0)
sl_result['Arudha_Lagna'] = sl_calc.calculate_arudha_lagna(asc_deg, first_lord_deg)
except Exception as e:
sl_result['Arudha_Lagna'] = {"error": str(e)}
try:
tenth_house_sign_idx = (asc_idx + 9) % 12
tenth_lord = SIGN_LORDS.get(SIGNS[tenth_house_sign_idx], '')
tenth_lord_deg = planet_lons.get(tenth_lord, 0)
sl_result['A10_Karma_Pada'] = sl_calc.calculate_a10(asc_deg, tenth_lord_deg)
except Exception as e:
sl_result['A10_Karma_Pada'] = {"error": str(e)}
try:
twelfth_house_sign_idx = (asc_idx + 11) % 12
twelfth_lord = SIGN_LORDS.get(SIGNS[twelfth_house_sign_idx], '')
twelfth_lord_deg = planet_lons.get(twelfth_lord, 0)
sl_result['Upapada_Lagna'] = sl_calc.calculate_upapada_lagna(asc_deg, twelfth_lord_deg)
except Exception as e:
sl_result['Upapada_Lagna'] = {"error": str(e)}
report['modules']['special_lagnas'] = sl_result
except Exception as e:
report['errors'].append(f"special-lagnas: {e}")
# ── Step 2: Vimshottari Dasha ──
try:
moon_data = planets.get('Moon', {})
moon_lon = moon_data.get('degree_raw', moon_data.get('degree', 0))
nak_idx = int(moon_lon / (360 / 27)) % 27
nak_name = NAKSHATRA_LIST[nak_idx][0]
nak_lord = NAKSHATRA_LIST[nak_idx][1]
pada = int((moon_lon % (360/27)) / (360/108)) + 1
birthdate = f"{args.year}-{args.month:02d}-{args.day:02d}"
today_str = getattr(args, 'transit_date', None) or getattr(args, 'today', None) or datetime.now().strftime('%Y-%m-%d')
dasha_result = cmd_dasha(type('Args', (), {
'nakshatra': nak_name, 'pada': pada,
'moon_lon': moon_lon,
'birthdate': birthdate,
'year': args.year,
'month': args.month,
'day': args.day,
'hour': args.hour,
'minute': args.minute,
'second': _arg_second(args),
'lat': args.lat,
'lon': args.lon,
'tz': args.tz,
'node_mode': getattr(args, 'node_mode', 'mean'),
'today': today_str
})())
report['modules']['dasha'] = dasha_result
report['modules']['dasha_sandhi'] = _calc_dasha_sandhi(dasha_result, today_str)
except Exception as e:
report['errors'].append(f"dasha: {e}")
# ── Step 3: Yoga格局 ──
try:
yoga_planets = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd and 'house' in pd:
yoga_planets[pn] = {'sign': pd['sign'], 'house': pd['house']}
yoga_result = cmd_yoga(type('Args', (), {
'ascendant': asc_sign,
'planets': ','.join([f"{k}:{v['sign']}:{v['house']}" for k, v in yoga_planets.items()])
})())
report['modules']['yoga'] = yoga_result
except Exception as e:
report['errors'].append(f"yoga: {e}")
# ── Step 4: BPHS十六分盘 ──
try:
sys.path.insert(0, SCRIPT_DIR)
from varga import calc_all_vargas
varga_result = calc_all_vargas(planet_lons, asc_deg, None) # None = 全部分盘
# 补充 D1_Rashi 到 varga_full(使用 chart 中的行星位置)
d1_data = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd:
d1_data[pn] = {
"sign": pd["sign"],
"degree": pd.get("degree", 0),
"degree_in_sign": pd.get("degree_in_sign", pd.get("degree", 0) % 30),
"house": pd.get("house"),
"nakshatra": pd.get("nakshatra"),
"nakshatra_pada": pd.get("nakshatra_pada"),
}
if d1_data:
varga_result["D1_Rashi"] = d1_data
if "D11_Rudramsa" not in varga_result:
try:
from divisional_charts_extended import DivisionalChartsCalculator, VargaType
dc_calc_for_d11 = DivisionalChartsCalculator()
varga_result["D11_Rudramsa"] = dc_calc_for_d11._calculate_single_varga(
VargaType.D11,
planet_lons,
asc_deg,
)
except Exception as d11_error:
varga_result["D11_Rudramsa"] = {"error": str(d11_error)}
d11 = varga_result.get("D11_Rudramsa")
if isinstance(d11, dict) and isinstance(d11.get("Ascendant"), dict):
asc_sign = d11["Ascendant"].get("sign_idx")
if isinstance(asc_sign, int):
d11["planets"] = {
name: {**data, "house": ((int(data["sign_idx"]) - asc_sign) % 12) + 1}
for name, data in d11.items()
if name not in {"_meta", "Ascendant", "planets"}
and isinstance(data, dict)
and isinstance(data.get("sign_idx"), int)
}
report['modules']['varga_full'] = varga_result
# v6.1.7: Re-run Yoga with D9/D60 context after varga-full is available.
# The earlier Step 3 remains a D1-only fallback for backward compatibility.
try:
yoga_context = _build_yoga_context_from_vargas(varga_result, planet_lons)
yoga_planets = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd and 'house' in pd:
yoga_planets[pn] = {
'sign': pd['sign'],
'house': pd['house'],
'degree': pd.get('degree_in_sign', pd.get('degree')),
}
yoga_result = cmd_yoga(type('Args', (), {
'ascendant': asc_sign,
'planets': ','.join([
f"{k}:{v['sign']}:{v['house']}" + (f":{v['degree']}" if v.get('degree') is not None else "")
for k, v in yoga_planets.items()
]),
'context_json': json.dumps(yoga_context, ensure_ascii=False),
})())
yoga_result['context_layers'] = sorted(k for k, v in yoga_context.items() if v)
report['modules']['yoga'] = yoga_result
except Exception as yoga_ctx_e:
report['errors'].append(f"yoga-context-injection: {yoga_ctx_e}")
report['modules']['vargottama'] = _calc_vargottama(planets, varga_result)
report['modules']['pushkara'] = _calc_pushkara_flags(planets)
report['modules']['sensitive_points'] = _calc_sensitive_points(planets)
except Exception as e:
report['errors'].append(f"varga-full: {e}")
# ── Step 4.5: Vimsopaka Bala 分盘力量 (v4.4.0) ──
try:
from vimsopaka_calculator import VimsopakaBalaCalculator, VargaType as VimsVargaType, DignityLevel
vims_calc = VimsopakaBalaCalculator(mode="shodasavarga")
# 将 varga_result 转换为 Vimsopaka 所需的 planet_vargas 格式
# varga_result 结构: {varga_name: {planet: {sign, degree, ...}}}
# 需要转为: {planet: {VargaType: DignityLevel}}
dignity_map = {
'EXALTED': DignityLevel.EXALTED, 'MOOLATRIKONA': DignityLevel.MOOLATRIKONA,
'OWN_SIGN': DignityLevel.OWN_SIGN, 'GREAT_FRIEND': DignityLevel.GREAT_FRIEND,
'FRIEND': DignityLevel.FRIEND, 'NEUTRAL': DignityLevel.NEUTRAL,
'ENEMY': DignityLevel.ENEMY, 'GREAT_ENEMY': DignityLevel.GREAT_ENEMY,
'DEBILITATED': DignityLevel.DEBILITATED, 'NEECHA_BHANGA': DignityLevel.NEECHA_BHANGA,
}
# 建立 VargaType → varga_result key 的映射(修复:每个分盘独立计算尊贵)
_vt_div = {VimsVargaType.D1:1, VimsVargaType.D2:2, VimsVargaType.D3:3,
VimsVargaType.D4:4, VimsVargaType.D7:7, VimsVargaType.D9:9,
VimsVargaType.D10:10, VimsVargaType.D12:12, VimsVargaType.D16:16,
VimsVargaType.D20:20, VimsVargaType.D24:24, VimsVargaType.D27:27,
VimsVargaType.D30:30, VimsVargaType.D40:40, VimsVargaType.D45:45,
VimsVargaType.D60:60}
_div_meta = {2:'Hora',3:'Drekkana',4:'Turyamsa',7:'Saptamsa',9:'Navamsa',
10:'Dasamsa',12:'Dwadashamsa',16:'Shodasamsa',20:'Vimsamsa',
24:'Siddhamsa',27:'Bhamsa',30:'Trimsamsa',40:'Khavedamsa',
45:'Akshavedamsa',60:'Shashtiamsa'}
planet_vargas_input = {}
for pn in planet_lons:
planet_vargas_input[pn] = {}
for vt in VimsVargaType:
div = _vt_div.get(vt)
if div and varga_result:
# 构造对应的 key,如 D9_Navamsa
vkey = f"D{div}_{_div_meta.get(div, f'D{div}')}"
vdata = varga_result.get(vkey, {})
# 使用 _get_dignity_level 计算完整5-fold尊贵(含Friend/Enemy
# 不使用 _dignity 字典(只有 Exalted/Debilitated/Own Sign
pinfo = vdata.get(pn, {})
if isinstance(pinfo, dict) and 'sign' in pinfo:
dignity_context = planets if div == 1 else _build_dignity_context(vdata)
dl_key = _get_dignity_level(pn, pinfo['sign'],
pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30),
dignity_context)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
elif div == 1:
# D1 直接用本命盘数据
p_sign = SIGNS[int(planet_lons.get(pn, 0) / 30) % 12]
d_in_s = planet_lons.get(pn, 0) % 30
dl_key = _get_dignity_level(pn, p_sign, d_in_s, planets)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
else:
planet_vargas_input[pn][vt] = DignityLevel.NEUTRAL
elif div == 1 and not varga_result:
# fallback: D1 从 planet_lons 计算
p_sign = SIGNS[int(planet_lons.get(pn, 0) / 30) % 12]
d_in_s = planet_lons.get(pn, 0) % 30
dl_key = _get_dignity_level(pn, p_sign, d_in_s, planets)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
else:
planet_vargas_input[pn][vt] = DignityLevel.NEUTRAL
vimsopaka_result = vims_calc.calculate_vimsopaka_bala(planet_vargas_input)
# 添加顶层汇总
if isinstance(vimsopaka_result, dict):
total_scores = []
for pn, pdata in vimsopaka_result.items():
if isinstance(pdata, dict) and 'total_score' in pdata:
total_scores.append(pdata['total_score'])
if total_scores:
avg_score = round(sum(total_scores) / len(total_scores), 2)
vimsopaka_result['total_score'] = avg_score
vimsopaka_result['total_score_max'] = 20.0
vimsopaka_result['status'] = '优秀' if avg_score >= 15 else '良好' if avg_score >= 10 else '一般' if avg_score >= 7 else '偏弱'
report['modules']['vimsopaka'] = vimsopaka_result
except Exception as e:
report['errors'].append(f"vimsopaka: {e}")
# ── Step 4.6: Divisional Charts Extended 扩展分盘 (v4.4.0) ──
try:
from divisional_charts_extended import DivisionalChartsCalculator
dc_calc = DivisionalChartsCalculator()
# 只计算 D5/D6/D8/D11(扩展分盘,与 Step 4 的标准分盘互补)
ext_vargas = dc_calc.calculate_all_vargas(planet_lons, asc_deg)
# 过滤只保留 Step 4 没有的分盘
ext_filtered = {k: v for k, v in ext_vargas.items()
if v.get('division') in [5, 6, 8, 11]}
report['modules']['varga_extended'] = ext_filtered
except Exception as e:
report['errors'].append(f"varga-extended: {e}")
# ── Step 4.7: Dispositor Chains + Inter-chart Linkage (v6.0.12) ──
try:
# 准备分盘数据:从 varga_full 提取 D1/D9/D10/D12
vf = report['modules'].get('varga_full', {})
d1_for_dc = vf.get('D1_Rashi', {})
d9_for_ic = vf.get('D9_Navamsa', {}) if isinstance(vf.get('D9_Navamsa'), dict) else {}
d10_for_ic = vf.get('D10_Dasamsa', {}) if isinstance(vf.get('D10_Dasamsa'), dict) else {}
d12_for_ic = vf.get('D12_Dwadashamsa', {}) if isinstance(vf.get('D12_Dwadashamsa'), dict) else None
# Dispositor Chains:基于 D1 行星数据
dc_result = calc_all_dispositor_chains(planets)
report['modules']['dispositor_chains'] = dc_result
# Inter-chart Linkage:需要各分盘的 asc_idx
# D1 asc_idx 已知;D9/D10/D12 的 asc_idx 需要从各自分盘数据提取
d9_asc_idx = None
d10_asc_idx = None
d12_asc_idx = None
if isinstance(d9_for_ic, dict):
d9_asc = d9_for_ic.get('Ascendant', {})
if isinstance(d9_asc, dict) and 'sign' in d9_asc:
d9_asc_idx = SIGNS.index(d9_asc['sign']) if d9_asc['sign'] in SIGNS else None
if isinstance(d10_for_ic, dict):
d10_asc = d10_for_ic.get('Ascendant', {})
if isinstance(d10_asc, dict) and 'sign' in d10_asc:
d10_asc_idx = SIGNS.index(d10_asc['sign']) if d10_asc['sign'] in SIGNS else None
if d12_for_ic and isinstance(d12_for_ic, dict):
d12_asc = d12_for_ic.get('Ascendant', {})
if isinstance(d12_asc, dict) and 'sign' in d12_asc:
d12_asc_idx = SIGNS.index(d12_asc['sign']) if d12_asc['sign'] in SIGNS else None
ic_result = calc_all_inter_chart_linkages(
d1_for_dc, d9_for_ic, d10_for_ic, d12_for_ic,
asc_idx, d9_asc_idx, d10_asc_idx
)
report['modules']['inter_chart_linkage'] = ic_result
# 补充:每个行星的"最终定位星"Dispositor Chain 的最后一个非循环元素)
final_dispositors = {}
for pname, chain in dc_result.items():
if chain:
# 找到最后一个不重复的元素
seen = set()
last_valid = None
for item in chain:
if item['dispositor'] in seen:
break
seen.add(item['dispositor'])
last_valid = item
if last_valid:
final_dispositors[pname] = last_valid['dispositor']
else:
final_dispositors[pname] = chain[-1]['dispositor'] if chain else None
else:
final_dispositors[pname] = None
report['modules']['final_dispositors'] = final_dispositors
except Exception as e:
report['errors'].append(f"dispositor-chain+inter-chart: {e}")
# ── Step 4.8: Tajika Yogas + Sahams (v6.0.13) ──
try:
from tajika import calc_tajika_yogas, calc_all_sahams
# Seven-planet Tajika candidates require actual instantaneous speed.
tajika_planets = {
name: {"longitude": item.get("degree_raw", item.get("lon")), "speed": item.get("speed")}
for name, item in planets.items()
if isinstance(item, dict) and name in {"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"}
}
tc_yogas = calc_tajika_yogas(tajika_planets)
report['modules']['tajika_yogas'] = tc_yogas
# Sahams(特殊点)—— 需要出生时间
birth_dt = _birth_datetime_from_args(args)
if birth_dt and planet_lons:
sahams_result = calc_all_sahams(
planet_lons,
asc_deg,
birth_dt,
lat=getattr(args, 'lat', None),
lon=getattr(args, 'lon', None),
tz=getattr(args, 'tz', None),
)
report['modules']['sahams'] = sahams_result
else:
report['modules']['sahams'] = {'warning': 'birth_datetime or planet_lons missing, skip saham calc'}
except Exception as e:
report['errors'].append(f"tajika-yogas+sahams: {e}")
# ── Step 4.9: Yogas + Doshas + Special Lagnas (v6.0.14) ──
try:
from yogas_doshas import calc_all_yogas_doshas
# 准备参数
asc_sign = SIGNS[asc_idx] if 'asc_idx' in dir() else SIGNS[0]
asc_lord = SIGN_LORDS.get(asc_sign, '')
moon_data = planets.get('Moon', {})
moon_sign = moon_data.get('sign', '') if isinstance(moon_data, dict) else ''
moon_house = moon_data.get('house', 0) if isinstance(moon_data, dict) else 0
mars_data = planets.get('Mars', {})
mars_house = mars_data.get('house', 0) if isinstance(mars_data, dict) else 0
sun_data = planets.get('Sun', {})
sun_house = sun_data.get('house', 0) if isinstance(sun_data, dict) else 0
# 12宫主星
h12_lord = SIGN_LORDS.get(SIGNS[(asc_idx + 11) % 12], '')
yd_result = calc_all_yogas_doshas(
planets, houses,
asc_sign, asc_lord,
moon_sign, moon_house,
mars_house, sun_house,
h12_lord
)
report['modules']['yogas_doshas'] = yd_result
except Exception as e:
report['errors'].append(f"yogas-doshas: {e}")
# ── Step 4.10: Tithi Lord + Pancha Pakshi + Rashi Tulya Navamsa (v6.0.15) ──
try:
# Tithi Lord(出生 Tithi + Lord 分析)
from tithi_lord import calc_tithi_lord_full
sun_deg = planet_lons.get('Sun', 0)
moon_deg = planet_lons.get('Moon', 0)
tithi_result = calc_tithi_lord_full(sun_deg, moon_deg, planets, houses)
report['modules']['tithi_lord'] = tithi_result
except Exception as e:
report['errors'].append(f"tithi-lord: {e}")
try:
# Pancha Pakshi(五鸟系统,需要出生 Nakshatra)
# v6.1.6: 对齐 pancha_pakshi.py 现有公共接口 get_pancha_pakshi_schedule()
moon_deg = planet_lons.get('Moon', 0)
nak_num = int(moon_deg / (360.0 / 27)) + 1
if nak_num > 27:
nak_num = 27
nak_name = NAKSHATRA_LIST[nak_num - 1][0]
tithi_number = int(((moon_deg - planet_lons.get('Sun', 0)) % 360) / 12) + 1
paksha = 'shukla' if 1 <= tithi_number <= 15 else 'krishna'
from pancha_pakshi import get_pancha_pakshi_schedule
pk_result = get_pancha_pakshi_schedule(
birth_nakshatra=nak_name,
paksha=paksha,
date=f"{args.year}-{args.month:02d}-{args.day:02d}",
)
pk_result['input_context'] = {
'moon_nakshatra_index': nak_num - 1,
'moon_nakshatra': nak_name,
'tithi_number': tithi_number,
'paksha': paksha,
}
report['modules']['pancha_pakshi'] = pk_result
except Exception as e:
report['errors'].append(f"pancha-pakshi: {e}")
try:
# Rashi Tulya NavamsaD1 与 D9 同宫对比分析)
# v6.1.6: 对齐 rashi_tulya_navamsa.py 现有公共接口 analyze_rtn(chart_data)
from rashi_tulya_navamsa import analyze_rtn
varga_full = report['modules'].get('varga_full', {})
d9_data = varga_full.get('D9_Navamsa', {})
if d9_data:
d9_planets = d9_data.get('planets') if isinstance(d9_data, dict) else None
if not d9_planets and isinstance(d9_data, dict):
d9_planets = {
pn: pd for pn, pd in d9_data.items()
if isinstance(pd, dict) and pn not in ('_meta', 'Ascendant') and 'sign' in pd
}
if d9_planets:
rt_chart = {
'ascendant': chart.get('ascendant', {}),
'planets': planets,
'context': {'navamsa_planets': d9_planets},
}
rt_result = analyze_rtn(rt_chart)
report['modules']['rashi_tulya_navamsa'] = {
'analysis': rt_result,
'summary': {
'strength_score': rt_result.get('strength_score'),
'weakness_score': rt_result.get('weakness_score'),
'exalted_cancelled_count': len(rt_result.get('exalted_cancelled', [])),
'debilitated_cancelled_count': len(rt_result.get('debilitated_cancelled', [])),
},
}
else:
report['modules']['rashi_tulya_navamsa'] = {'note': 'D9 planet data incomplete, skip Rashi Tulya Navamsa'}
else:
report['modules']['rashi_tulya_navamsa'] = {'note': 'D9 data incomplete, skip Rashi Tulya Navamsa'}
except Exception as e:
report['errors'].append(f"rashi-tulya-navamsa: {e}")
# ── Step 4.11: Marriage Counting + Muntha + D30 + Prashna (v6.0.16) ──
try:
# Marriage Counting Method (Bhrigu)
from marriage_counting import marriage_counting_full_analysis
d1_house7_lord = houses.get('7', {}).get('lord', '') if isinstance(houses.get('7'), dict) else ''
if d1_house7_lord and 'varga_full' in report['modules']:
d9_data = report['modules']['varga_full'].get('D9_Navamsa', {})
d9_planet_lons = _varga_planet_lons(d9_data)
if d9_planet_lons:
mc_result = marriage_counting_full_analysis(
d1_house7_lord, planet_lons, d9_planet_lons,
houses, None
)
report['modules']['marriage_counting'] = mc_result
except Exception as e:
report['errors'].append(f"marriage-counting: {e}")
try:
# Bhrigu Pada Dasha(通用近似版,精确公式因流派而异)
from bhrigu_pada_dasha import bhrigu_pada_dasha_full_report
moon_lon = planet_lons.get('Moon', 0)
birth_jd = report.get('metadata', {}).get('birth_jd', 0) or 0
if moon_lon and birth_jd:
bpd_result = bhrigu_pada_dasha_full_report(
moon_lon, birth_jd,
d9_7lord_sign=None # 将在 D9 数据可用时补充
)
report['modules']['bhrigu_pada_dasha'] = bpd_result
except Exception as e:
report['errors'].append(f"bhrigu-pada-dasha: {e}")
try:
# Muntha (Tajika Varshaphala) — 需要 Varshaphala 盘数据
# Varshaphala 盘需另外排盘(太阳返照时刻),full-reading 中暂不可用
# 如需 Varshaphala 分析,请使用 cmd_prashna(独立命令支持 Varshaphala 排盘)
report['modules']['muntha'] = {
'note': 'Muntha 需要 Varshaphala(太阳返照)盘数据。请使用 cmd_prashna 独立命令获取 Varshaphala 分析。',
'varshaphala_available': False
}
except Exception as e:
report['errors'].append(f"muntha: {e}")
try:
# D30 Trimshamsa analysis
from trimshamsa_d30 import d30_full_report
d30_result = d30_full_report(planet_lons)
report['modules']['trimshamsa_d30'] = d30_result
except Exception as e:
report['errors'].append(f"trimshamsa-d30: {e}")
try:
# Prashna(问事占星)—— full-reading 中暂不接入
# 原因:Prashna 需要问卜时刻(用户提问时间)排盘,full-reading 只有出生数据
# 如需 Prashna 分析:使用 cmd_prashna 独立命令
# 该命令接受问卜时间 + 地点,排 Prashna 盘并做分析
report['modules']['prashna'] = {
'note': 'Prashna 需要问卜时间排盘,请使用 cmd_prashna 独立命令。',
'cmd': 'cmd_prashna',
'available': True
}
except Exception as e:
report['errors'].append(f"prashna: {e}")
# ── Step 4.12: Solar Return (Varshaphala) + Muntha proper (v6.0.18) ──
try:
# Solar Return(太阳返照盘)计算
if hasattr(args, 'target_year') and args.target_year:
from solar_return import solar_return_full_report
sr_result = solar_return_full_report(
args.year, args.month, args.day,
args.hour, args.minute,
args.lat, args.lon, args.tz,
args.target_year,
ayanamsa_name=_current_ayanamsa_name(args),
)
report['modules']['solar_return'] = sr_result
# 更新 muntha 为正确值(来自太阳返照盘)
if 'muntha' in sr_result:
report['modules']['muntha'] = {
'source': 'solar_return_proper',
'muntha_sign': sr_result.get('muntha', {}).get('muntha_sign', '?'),
'muntha_lord': sr_result.get('muntha', {}).get('muntha_lord', '?'),
'note': '来自太阳返照盘的正确 Muntha 计算',
}
else:
report['modules']['solar_return'] = {
'note': '未提供 --target-year,跳过太阳返照盘计算',
'skipped': True,
'hint': '使用 --target-year <年份> 重新运行以获取 Varshaphala 分析'
}
except Exception as e:
report['errors'].append(f"solar-return: {e}")
# ── Step 4.13: Narayana DashaRishi Dashav6.0.20)──
try:
from narayana_dasha import narayana_dasha_full_report
# 计算当前年龄(从出生到 today)
birth_dt = _birth_datetime_from_args(args)
today_dt = datetime.strptime(args.today, '%Y-%m-%d') if hasattr(args, 'today') and args.today else datetime.now()
current_age = (today_dt - birth_dt).days / 365.25
narayana_result = narayana_dasha_full_report(
lagna_sign_idx=asc_idx,
planet_lons=planet_lons,
current_age=current_age,
birth_year=args.year,
)
report['modules']['narayana_dasha'] = narayana_result
except Exception as e:
report['errors'].append(f"narayana-dasha: {e}")
_record_stage_timing(
stage_timings,
'dasha_and_core_varga_stack',
stage_started,
enabled=profile_stages,
status='error' if any(err.startswith(('special-lagnas:', 'dasha:', 'yoga:', 'varga-full:', 'vimsopaka:', 'varga-extended:', 'dispositor-chain+inter-chart:', 'tajika-yogas+sahams:', 'yogas-doshas:', 'tithi-lord:', 'pancha-pakshi:', 'rashi-tulya-navamsa:', 'marriage-counting:', 'bhrigu-pada-dasha:', 'muntha:', 'trimshamsa-d30:', 'prashna:', 'solar-return:', 'narayana-dasha:')) for err in report['errors']) else 'ok',
details={'through_step': '4.13'},
)
# ── Step 5: 精确相位 ──
stage_started = time.perf_counter()
try:
from aspects import calc_all_aspects
aspects_result = calc_all_aspects(planet_lons, asc_deg)
report['modules']['aspects'] = aspects_result
except Exception as e:
report['errors'].append(f"aspects: {e}")
# ── Step 6: Jaimini系统 ──
try:
from jaimini import (calc_chara_karaka_7, calc_chara_karaka_8, calc_chara_dasha,
calc_karakamsha, calc_chara_dasha_with_antardasha,
calc_arudha_padas, calc_graha_padas, calc_special_lagnas)
from varga import calc_varga
jaimini_result = {}
jaimini_result['chara_karaka_7'] = calc_chara_karaka_7(planet_degs)
jaimini_result['chara_karaka_8'] = calc_chara_karaka_8(planet_degs)
# Step 6+: Karaka JH Compatible Mode (v4.4.0)
try:
from karaka_calculator import KarakaCalculator, KarakaMode
kc_jh = KarakaCalculator(mode=KarakaMode.JH_COMPATIBLE)
jaimini_result['chara_karaka_jh'] = kc_jh.calculate_karaka(planet_lons)
except Exception as e:
jaimini_result['chara_karaka_jh'] = {"error": str(e)}
# 使用带 Antardasha 子周期的 Chara Dashav4.3.0v6.1.12 KN Rao benchmark 通过)
jaimini_result['chara_dasha'] = calc_chara_dasha_with_antardasha(asc_idx, planet_lons, args.year, args.month)
jaimini_result['has_antardasha'] = True
jaimini_result['chara_dasha_capability'] = {
'status': 'covered',
'reason': 'KN Rao benchmark overall match 95.83%; Aquarius/Scorpio co-lord strength arbitration remains a documented edge case.',
'usage_rule': 'Use Chara Karaka/Karakamsha normally; use Chara Dasha timing with Vimshottari/Transit/Varga corroboration.'
}
# Karakamsha(用AK灵魂星,非DK配偶星)
# ⚠️ 2026-05-03修正:此前错误使用DK,现已修正为AK
ck7 = jaimini_result['chara_karaka_7']
ak_name = ck7['karaka_table']['Atmakaraka']['planet']
ak_lon = planet_lons.get(ak_name, 0)
ak_d9 = calc_varga(ak_lon, 9)
jaimini_result['karakamsha'] = calc_karakamsha(
ak_d9.get('sign', 'Aries'), ak_d9.get('degree_in_sign', 0))
jaimini_result['arudha_padas'] = calc_arudha_padas(asc_idx, planet_lons)
jaimini_result['graha_padas'] = calc_graha_padas(planet_lons)
jaimini_result['special_lagnas'] = calc_special_lagnas(asc_idx, args.hour, args.minute + _arg_second(args) / 60.0)
# Darakaraka 深度解读(v6.1.10
# Registry 已将 modules.jaimini.darakaraka 标为 covered;这里把独立 DK
# reader 接入 full-reading,使婚姻主题报告可消费真实 DK 画像、D9 状态、
# 合相影响和质量评分,而不是只停留在 chara_karaka 表格层。
try:
from darakaraka_reader import analyze_darakaraka
d9_planets_for_dk = {}
varga_data = report['modules'].get('varga_full', {})
d9_data = varga_data.get('D9_Navamsa', {}) if isinstance(varga_data, dict) else {}
d9_asc = d9_data.get('Ascendant', {}) if isinstance(d9_data, dict) else {}
d9_asc_idx = SIGNS.index(d9_asc.get('sign', 'Aries')) if isinstance(d9_asc, dict) and d9_asc.get('sign') in SIGNS else 0
d9_context = _build_dignity_context(d9_data)
if isinstance(d9_data, dict):
for d9_pn, d9_pd in d9_data.items():
if d9_pn == '_meta' or d9_pn == 'Ascendant' or not isinstance(d9_pd, dict) or 'sign' not in d9_pd:
continue
d9_sign = d9_pd.get('sign')
d9_deg = d9_pd.get('degree_in_sign', d9_pd.get('degree', 0) % 30)
d9_sign_idx = SIGNS.index(d9_sign) if d9_sign in SIGNS else 0
d9_planets_for_dk[d9_pn] = {
'sign': d9_sign,
'house': ((d9_sign_idx - d9_asc_idx) % 12) + 1,
'degree_in_sign': d9_deg,
'dignity': _get_dignity_level(d9_pn, d9_sign, d9_deg, d9_context),
}
jaimini_result['darakaraka'] = analyze_darakaraka({
'ascendant': chart.get('ascendant', {}),
'planets': planets,
'context': {'navamsa_planets': d9_planets_for_dk},
}, use_8_karaka=True)
except Exception as dk_e:
jaimini_result['darakaraka'] = {'error': str(dk_e)}
report['modules']['jaimini'] = jaimini_result
except Exception as e:
report['errors'].append(f"jaimini: {e}")
# ── Step 7: 高级Nakshatrav6.0.22 升级:含 Chandra Bala + 综合报告)──
try:
from nakshatra_advanced import nakshatra_full_report as nakshatra_report
nak_full = nakshatra_report(chart)
report['modules']['nakshatra_advanced'] = nak_full
except Exception as e:
report['errors'].append(f"nakshatra-adv: {e}")
# ── Step 7.5: Nakshatra Dasha 星宿大运(v6.0.22 新增)──
try:
from nakshatra_dasha import nakshatra_dasha_full_report
birth_date_str = f"{args.year}-{args.month:02d}-{args.day:02d}"
# 年龄
nak_age = args.age
if nak_age is None:
try:
birth_date = datetime(args.year, args.month, args.day)
nak_age = (datetime.now() - birth_date).days / 365.25
except:
nak_age = None
if nak_age is not None:
# 获取过境星宿数据
transit_lons_for_nak = None
try:
transit_ref = getattr(args, 'transit_date', None) or datetime.now().strftime('%Y-%m-%d')
ty, tm, td = map(int, transit_ref.split('-'))
transit_jd_nak = swe.julday(ty, tm, td, 12.0 - args.tz)
transit_pl_nak, _ = _calc_sidereal_planets_for_jd(
transit_jd_nak, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True)
transit_lons_for_nak = {}
for pn, pd in transit_pl_nak.items():
if isinstance(pd, dict) and 'degree' in pd:
transit_lons_for_nak[pn] = pd['degree']
except:
pass
nak_dasha = nakshatra_dasha_full_report(
chart, birth_date_str, nak_age, transit_lons_for_nak)
report['modules']['nakshatra_dasha'] = nak_dasha
except Exception as e:
report['errors'].append(f"nakshatra-dasha: {e}")
# ── Step 8: Argala门闩 ──
try:
from argala import calc_argala
report['modules']['argala'] = calc_argala(planet_sign_indices, asc_idx)
except Exception as e:
report['errors'].append(f"argala: {e}")
# ── Step 9: Tajika年运盘(需要年龄) ──
age = args.age
if age is None:
# 自动计算年龄
try:
birth_date = datetime(args.year, args.month, args.day)
age = (datetime.now() - birth_date).days // 365
report['warnings'].append(f"未提供年龄,自动计算为 {age}")
except:
age = None
if age is not None:
try:
from tajika import calc_muntha, calc_year_lord, calc_mudda_dasha, calc_tri_pataka
asc_si = int(asc_deg / 30) % 12
tajika_result = {}
tajika_result['muntha'] = calc_muntha(asc_si, age)
yl = calc_year_lord(asc_si, age)
tajika_result['year_lord'] = yl
varsha_lord = yl.get('year_lord', 'Jupiter')
tajika_result['mudda_dasha'] = calc_mudda_dasha(asc_si, varsha_lord, args.month)
muntha_si = (asc_si + age) % 12
tajika_result['tri_pataka'] = calc_tri_pataka(planet_lons, varsha_lord, muntha_si)
report['modules']['tajika'] = tajika_result
except Exception as e:
report['errors'].append(f"tajika: {e}")
# ── Step 10: Shadbala六重力量 ──
try:
from shadbala import build_shadbala_context, calc_shadbala
birth_hour = _birth_hour_decimal(args.hour, args.minute, _arg_second(args))
sun_lon = planet_lons.get('Sun', 0)
moon_lon = planet_lons.get('Moon', 0)
context = build_shadbala_context(jd, args.lat, args.lon, _current_ayanamsa_name(args), args.tz)
shadbala_result = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon, context=context)
# 添加顶层汇总
if isinstance(shadbala_result, dict):
sb_planets = shadbala_result.get('planets', {})
total_rupas = 0
min_req_total = 0
strong_count = 0
for pn, pdata in sb_planets.items():
if isinstance(pdata, dict):
total_rupas += pdata.get('total_rupas', 0)
min_req_total += pdata.get('min_required', 0)
if pdata.get('total_rupas', 0) >= pdata.get('min_required', 0):
strong_count += 1
shadbala_result['total_shadbala'] = round(total_rupas, 2)
shadbala_result['total_min_required'] = round(min_req_total, 2)
shadbala_result['strong_planets'] = strong_count
shadbala_result['weak_planets'] = len(sb_planets) - strong_count
shadbala_result['status'] = '优秀' if strong_count >= 5 else '良好' if strong_count >= 3 else '一般'
report['modules']['shadbala'] = shadbala_result
except Exception as e:
report['errors'].append(f"shadbala: {e}")
# ── Step 10.1: Remedies 补救建议 ──
try:
from remedies import recommend_remedies
shadbala_for_remedies = {}
for pn, pdata in (report['modules'].get('shadbala') or {}).get('planets', {}).items():
if isinstance(pdata, dict):
shadbala_for_remedies[pn] = {
'total_rupas': pdata.get('total_rupas', pdata.get('rupas', 1.0)),
'strength_level': pdata.get('strength_level', pdata.get('level', '')),
}
yd = report['modules'].get('yogas_doshas') or {}
doshas = []
dosha_sources = [
('mangal_dosha', 'Mangal Dosha'),
('kaal_sarp_dosha', 'Kaal Sarp Dosha'),
('pitra_dosha', 'Pitra Dosha'),
]
for key, label in dosha_sources:
item = yd.get(key) if isinstance(yd, dict) else None
if isinstance(item, dict) and item.get('has_dosha'):
doshas.append(label)
dasha = report['modules'].get('dasha') or {}
current_dasha = dasha.get('current_dasha') if isinstance(dasha, dict) else {}
active_dasha_lord = current_dasha.get('lord') if isinstance(current_dasha, dict) else None
report['modules']['remedies'] = recommend_remedies(
shadbala_for_remedies,
active_dasha_lord=active_dasha_lord,
doshas=doshas,
)
except Exception as e:
report['errors'].append(f"remedies: {e}")
# ── Step 10.5: Avasthas 行星状态 (v4.4.0) ──
try:
from avastha_calculator import AvasthaCalculator
avast_calc = AvasthaCalculator()
avast_result = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'sign' in pd and 'degree' in pd:
sign = pd['sign']
deg_in_sign = pd.get('degree_in_sign', pd['degree'] % 30)
house = pd.get('house', 0)
# 收集同宫行星作为 conjunctions
conjunctions = [p2 for p2, pd2 in planets.items()
if isinstance(pd2, dict) and pd2.get('house') == house and p2 != pn]
avast_result[pn] = avast_calc.calculate_all_avasthas(
planet=pn, sign=sign, degree=deg_in_sign,
house=house, conjunctions=conjunctions)
report['modules']['avasthas'] = avast_result
except Exception as e:
report['errors'].append(f"avasthas: {e}")
# ── Step 11: Ashtakavarga八分法 ──
asht_data = None
try:
from ashtakavarga import calc_ashtakavarga
asht_data = calc_ashtakavarga(planets, asc_idx)
report['modules']['ashtakavarga'] = asht_data
except Exception as e:
report['errors'].append(f"ashtakavarga: {e}")
# ── Step 12: R1-R10数学验证 ──
try:
from validate import validate_chart
report['modules']['validation'] = validate_chart(chart, asht_data)
except Exception as e:
report['errors'].append(f"validate: {e}")
# ── Step 13: P1-P12行星审计 ──
try:
audit_result = cmd_audit(args)
report['modules']['audit'] = audit_result
except Exception as e:
report['errors'].append(f"audit: {e}")
# ── Step 14: ⭐ Transit Actionable Context (v4.1.0) ──
try:
actionable_ctx = _calc_actionable_context(planets, asc_idx)
report['modules']['actionable_context'] = actionable_ctx
except Exception as e:
report['errors'].append(f"actionable-context: {e}")
# ── Step 15: Planetary Congregation 行星聚集 (v4.3.0) ──
try:
congregation = _calc_planetary_congregation(planets, asc_idx)
report['modules']['congregation'] = congregation
except Exception as e:
report['errors'].append(f"congregation: {e}")
# ── Step 16: Vivah Saham 婚姻敏感点 (v4.3.0) ──
try:
sahams = _calc_vivah_saham(planets, asc_deg)
# 包装为标准结构,与 standalone vivah-saham 子命令一致
if 'error' not in sahams:
report['modules']['vivah_saham'] = {
'vivah_saham': {
'longitude': sahams['saham_lon'],
'sign': sahams['saham_sign'],
'degree_in_sign': sahams['saham_deg_in_sign'],
'house': sahams['saham_house'],
},
**sahams
}
else:
report['modules']['vivah_saham'] = sahams
except Exception as e:
report['errors'].append(f"vivah-saham: {e}")
# ── Step 17: Transit 多参考点分析 (v6.0.10 true transit) ──
try:
transit_reference_date = getattr(args, 'transit_date', None) or getattr(args, 'today', None) or datetime.now().strftime('%Y-%m-%d')
ty, tm, td = map(int, transit_reference_date.split('-'))
transit_jd = swe.julday(ty, tm, td, 12.0 - args.tz)
transit_planets, transit_ayanamsa = _calc_sidereal_planets_for_jd(transit_jd, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True)
report['modules']['transit_positions'] = {
'method': 'Swiss Ephemeris true transit positions',
'target_date': transit_reference_date,
'ayanamsa': round(transit_ayanamsa, 4) if transit_ayanamsa is not None else None,
'node_mode': getattr(args, 'node_mode', 'mean'),
'data_layer': 'true_transit_positions',
'planets': transit_planets,
}
transit_multi = _calc_transit_multi_reference(planets, asc_idx, asc_deg, planet_lons, transit_planets=transit_planets, transit_date=transit_reference_date, node_mode=getattr(args, 'node_mode', 'mean'))
report['modules']['transit_multi_reference'] = transit_multi
except Exception as e:
report['errors'].append(f"transit-multi-ref: {e}")
# ── Step 18: Dasa Convergence 多系统交叉验证 (v6.1.6) ──
try:
dasha_data = report['modules'].get('dasha', {})
jaimini_data = report['modules'].get('jaimini', {})
chara_dasha_data = jaimini_data.get('chara_dasha', {}) if isinstance(jaimini_data, dict) else {}
moon_lon = planet_lons.get('Moon', 0)
moon_nakshatra_index = int(moon_lon / (360 / 27)) % 27
moon_pada = int((moon_lon % (360 / 27)) / (360 / 108)) + 1
tithi_number = int(((moon_lon - planet_lons.get('Sun', 0)) % 360) / 12) + 1
birth_info_for_alt_dasha = {
'birth_datetime': _birth_datetime_from_args(args),
'moon_nakshatra_index': moon_nakshatra_index,
'moon_pada': moon_pada,
'is_shukla_paksha': 1 <= tithi_number <= 15,
'lagna_rashi_index': asc_idx,
}
try:
from yogini_dasha import calculate_yogini_dasha
yogini_data = calculate_yogini_dasha(birth_info_for_alt_dasha)
report['modules']['yogini_dasha'] = yogini_data
except Exception as alt_e:
yogini_data = {'error': str(alt_e)}
report['modules']['yogini_dasha'] = yogini_data
try:
from ashtottari_dasha import calculate_ashtottari_dasha
ashtottari_data = calculate_ashtottari_dasha(birth_info_for_alt_dasha)
report['modules']['ashtottari_dasha'] = ashtottari_data
except Exception as alt_e:
ashtottari_data = {'error': str(alt_e)}
report['modules']['ashtottari_dasha'] = ashtottari_data
try:
from kalachakra_dasha import calculate_kalachakra_dasha
kalachakra_data = calculate_kalachakra_dasha(birth_info_for_alt_dasha)
report['modules']['kalachakra_dasha'] = kalachakra_data
except Exception as alt_e:
kalachakra_data = {'error': str(alt_e)}
report['modules']['kalachakra_dasha'] = kalachakra_data
convergence = _calc_dasa_convergence(
dasha_data,
chara_dasha_data,
yogini_data,
planet_lons,
asc_idx,
ashtottari_result=ashtottari_data,
kalachakra_result=kalachakra_data,
)
report['modules']['dasa_convergence'] = convergence
except Exception as e:
report['errors'].append(f"dasa-convergence: {e}")
# ── Step 19: D9 Navamsa 逐行星尊严展开 (v4.5.0 P1) ──
try:
varga_data = report['modules'].get('varga_full', {})
d9_data = varga_data.get('D9_Navamsa', {}) if isinstance(varga_data, dict) else {}
if d9_data:
d9_expanded = {}
d9_context = _build_dignity_context(d9_data)
for pn, pd in d9_data.items():
if pn == '_meta' or not isinstance(pd, dict) or 'sign' not in pd:
continue
d9_sign = pd['sign']
d9_deg = pd.get('degree_in_sign', pd.get('degree', 0) % 30)
dignity = _get_dignity_level(pn, d9_sign, d9_deg, d9_context)
# D9 宫位(从D9 Asc计算)
d9_asc_data = d9_data.get('Ascendant', {})
d9_asc_sign_idx = SIGNS.index(d9_asc_data.get('sign', 'Aries')) if isinstance(d9_asc_data, dict) and d9_asc_data.get('sign') in SIGNS else 0
p_sign_idx = SIGNS.index(d9_sign) if d9_sign in SIGNS else 0
d9_house = ((p_sign_idx - d9_asc_sign_idx) % 12) + 1
# 关系状态
d9_sign_lord = SIGN_LORDS.get(d9_sign, '')
is_own = (d9_sign_lord == pn)
is_exalted = (EXALTATION.get(pn) == d9_sign)
is_debilitated = (DEBILITATION.get(pn) == d9_sign)
is_moola = False
if pn in MOOLATRIKONA:
mt_sign, mt_start, mt_end = MOOLATRIKONA[pn]
if mt_sign == d9_sign and mt_start <= d9_deg < mt_end:
is_moola = True
d9_expanded[pn] = {
'sign': d9_sign,
'sign_cn': SIGNS_CN.get(d9_sign, ''),
'house_in_d9': d9_house,
'dignity': dignity,
'is_own_sign': is_own,
'is_exalted': is_exalted,
'is_debilitated': is_debilitated,
'is_moolatrikona': is_moola,
'pada': pd.get('pada'),
'lord': d9_sign_lord,
'degree_in_sign': round(d9_deg, 4),
}
report['modules']['d9_navamsa_expanded'] = d9_expanded
except Exception as e:
report['errors'].append(f"d9-expanded: {e}")
_record_stage_timing(
stage_timings,
'advanced_interpretation_and_timing_layers',
stage_started,
enabled=profile_stages,
status='error' if any(err.startswith(('aspects:', 'jaimini:', 'nakshatra-adv:', 'nakshatra-dasha:', 'argala:', 'tajika:', 'shadbala:', 'remedies:', 'avasthas:', 'ashtakavarga:', 'validate:', 'audit:', 'actionable-context:', 'congregation:', 'vivah-saham:', 'transit-multi-ref:', 'dasa-convergence:', 'd9-expanded:')) for err in report['errors']) else 'ok',
details={'through_step': '19'},
)
# ── 汇总 ──
stage_started = time.perf_counter()
# ── 生成动态引导 (Dynamic Hooks) ──
try:
report['dynamic_hooks'] = generate_life_stage_hooks(
planets=report['modules'].get('chart', {}).get('planets', {}),
asc_sign=report['modules'].get('chart', {}).get('ascendant', {}).get('sign', ''),
asc_idx=report['modules'].get('chart', {}).get('ascendant', {}).get('sign_idx', 0),
current_dasha=report['modules'].get('dasha', {}).get('current_dasha', {}),
narayana_dasha=report['modules'].get('narayana_dasha', {})
)
except Exception as e:
report['dynamic_hooks'] = []
report['errors'].append(f"hook_engine: {e}")
_record_stage_timing(
stage_timings,
'dynamic_hooks',
stage_started,
enabled=profile_stages,
status='error' if any(err.startswith('hook_engine:') for err in report['errors']) else 'ok',
)
report['summary'] = {}
stage_started = time.perf_counter()
try:
_attach_vedastro_official_full_snapshot(report, args)
except Exception as e:
report['warnings'].append(f"vedastro-official-full-snapshot: {e}")
_record_stage_timing(
stage_timings,
'vedastro_official_snapshot',
stage_started,
enabled=profile_stages,
status='warning' if any(warn.startswith('vedastro-official-full-snapshot:') for warn in report['warnings']) else 'ok',
)
stage_started = time.perf_counter()
try:
_attach_vedastro_main_entry_overview(report, args)
except Exception as e:
report['warnings'].append(f"vedastro-main-entry-overview: {e}")
_record_stage_timing(
stage_timings,
'vedastro_main_entry_overview',
stage_started,
enabled=profile_stages,
status='warning' if any(warn.startswith('vedastro-main-entry-overview:') for warn in report['warnings']) else 'ok',
)
stage_started = time.perf_counter()
try:
strict_evidence_collector = _load_strict_evidence_collector()
for route in ('relationship', 'career', 'finance'):
module_name = STRICT_WORKFLOW_MODULE_MAP[route]
report['modules'][module_name] = strict_evidence_collector(route, report)
report['modules']['career_strict_evidence']['user_narrative'] = _build_career_narrative_payload(
report['modules']['career_strict_evidence']
)
report['modules']['relationship_strict_evidence']['user_narrative'] = _build_relationship_narrative_payload(
report['modules']['relationship_strict_evidence']
)
report['modules']['finance_strict_evidence']['user_narrative'] = _build_finance_narrative_payload(
report['modules']['finance_strict_evidence']
)
except Exception as e:
report['errors'].append(f"strict-evidence-collector: {e}")
_record_stage_timing(
stage_timings,
'strict_contracts',
stage_started,
enabled=profile_stages,
status='error' if any(err.startswith('strict-evidence-collector:') for err in report['errors']) else 'ok',
)
stage_started = time.perf_counter()
try:
report['modules']['guided_topics'] = build_guided_topics(report)
except Exception as e:
report['warnings'].append(f"guided-topics: {e}")
report['modules']['guided_topics'] = []
_record_stage_timing(
stage_timings,
'guided_topics',
stage_started,
enabled=profile_stages,
status='warning' if any(warn.startswith('guided-topics:') for warn in report['warnings']) else 'ok',
)
stage_started = time.perf_counter()
report['ai_prompt_pack'] = _build_ai_prompt_pack(report)
_record_stage_timing(
stage_timings,
'ai_prompt_pack',
stage_started,
enabled=profile_stages,
)
elapsed = round(time.perf_counter() - t0, 4)
module_count = len(report['modules'])
error_count = len(report['errors'])
slowest_stages = sorted(stage_timings, key=lambda item: item.get('elapsed_seconds', 0), reverse=True)[:5]
unified_stage_contract = _build_unified_stage_contract(stage_timings)
report['summary'] = {
'elapsed_seconds': elapsed,
'modules_computed': module_count,
'errors': error_count,
'status': 'complete' if error_count == 0 else f'{error_count} errors',
'stage_timing_enabled': True,
'stage_timings': stage_timings,
'slowest_stages': slowest_stages,
'guided_topics': report['modules'].get('guided_topics', []),
**unified_stage_contract,
'next_step': '⭐ v6.1.6: full-reading 已输出 transit_multi_reference(四参考点) + dasa_convergence(五系统交叉) + yogini_dasha + ashtottari_dasha + kalachakra_dasha + d9_navamsa_expanded。AI必须使用四参考点分析Transit,Dasa预测必须标注多系统收敛等级。',
}
return report
# ============================================================================
# 23. Prashna 问事占星 (v3.9新增)
# ============================================================================
def cmd_prashna(args):
"""Prashna 问事占星:基于提问时刻的即时星盘分析"""
try:
from prashna_context import PrashnaContextError, build_prashna_context
except ImportError:
from scripts.prashna_context import PrashnaContextError, build_prashna_context
try:
context = build_prashna_context({
"question_text": args.question_text,
"question_timestamp": args.datetime,
"lat": args.lat,
"lon": args.lon,
"timezone": args.timezone,
"ayanamsa": args.ayanamsa,
"node_mode": args.node_mode,
"location_convention": args.location_convention,
})
except PrashnaContextError as exc:
return {"scope": "prashna_context", "status": "blocked", "reason": str(exc)}
if args.mode != "chart":
return {
"scope": "prashna",
"status": "blocked",
"reason": f"{args.mode} is blocked pending validated Prashna kernel implementation",
"prashna_context": context,
}
return context
# ============================================================================
# Sudarshana Chakra (v6.9.14新增)
# ============================================================================
def cmd_sudarshana(args):
"""Sudarshana Chakra 三参考点盘分析"""
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
if chart is None:
return {"error": "swisseph未安装"}
# 构造 planet_lons 和 asc_lon
planet_lons = {}
for pname, pdata in chart.get('planets', {}).items():
if isinstance(pdata, dict) and 'degree_raw' in pdata:
planet_lons[pname] = pdata['degree_raw']
asc_data = chart.get('ascendant', {})
asc_lon = asc_data.get('degree_raw', asc_data.get('degree', 0))
if asc_lon == 0:
asc_lon = asc_idx * 30.0
house = getattr(args, 'house', None)
if getattr(args, 'text', False):
report = generate_sudarshana_report(planet_lons, asc_lon)
print(report)
return {"format": "text", "report_printed": True}
return calc_sudarshana_chakra(planet_lons, asc_lon, house=house)
# ============================================================================
# CLI入口
# ============================================================================
def main():
parser = argparse.ArgumentParser(description='印度占星统一引擎 v6.9.14', formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest='command', help='子命令')
# 1. chart
p = sub.add_parser('chart', help='计算完整星盘')
_add_chart_args(p)
p.add_argument('--validate', action='store_true', help='附加R1-R10数学验证')
p.add_argument('--table', action='store_true', help='以 ASCII 表格输出核心排盘结果')
# 2. dasha
p = sub.add_parser('dasha', help='计算Dasha大运')
# Birth datetime args: required only when nakshatra/moon-lon not provided
p.add_argument('--year', type=int, required=False)
p.add_argument('--month', type=int, required=False)
p.add_argument('--day', type=int, required=False)
p.add_argument('--hour', type=int, required=False)
p.add_argument('--minute', type=int, required=False)
p.add_argument('--second', type=_second_arg, default=0)
p.add_argument('--lat', type=float, required=False)
p.add_argument('--lon', type=float, required=False)
p.add_argument('--tz', type=float, default=None)
p.add_argument('--node-mode', default='mean', choices=['mean', 'true'])
p.add_argument('--ayanamsa', default='lahiri', choices=list(AYANAMSA_MODES.keys()),
help='恒星黄道系统(默认lahiri)。可选: raman, kp, fagan_bradley, djwhal_khul, sassanian, true_citra')
p.add_argument('--nakshatra', default=None); p.add_argument('--pada', type=int, default=None)
p.add_argument('--moon-lon', type=float, default=None); p.add_argument('--birthdate', default=None)
p.add_argument('--today', default=None)
p.add_argument('--years', type=int, default=10)
p.add_argument('--table', action='store_true', help='以 ASCII 表格输出大运主时间线')
# 3. yoga
p = sub.add_parser('yoga', help='Yoga格局识别')
_add_chart_args(p)
p.add_argument('--ascendant', default=None)
p.add_argument('--planets', default=None, help="格式: 'Sun:Aries:9[:10.5],Moon:Aquarius:7[:15.2],...';若提供则优先使用并忽略出生信息")
p.add_argument('--context-json', default=None, help='可选:传入包含 d9/d60/panchanga 的 YogaContext JSON')
for action in p._actions:
if action.dest in ['year', 'month', 'day', 'hour', 'minute', 'lat', 'lon']:
action.required = False
# 4. predict
p = sub.add_parser('predict', help='三层验证法事件预测')
p.add_argument('--chart', default=None, help='星盘JSON字符串')
p.add_argument('--event-type', default='all', choices=['all', 'marriage', 'career', 'wealth', 'health'])
p.add_argument('--past-verify', action='store_true', help='验前事模式:推断2-4个高信号历史时段')
p.add_argument('--year', type=int, default=None, help='出生年(验前事模式需要)')
p.add_argument('--month', type=int, default=None, help='出生月')
p.add_argument('--day', type=int, default=None, help='出生日')
p.add_argument('--hour', type=int, default=None, help='出生时')
p.add_argument('--minute', type=int, default=None, help='出生分')
p.add_argument('--second', type=_second_arg, default=0, help='出生秒')
p.add_argument('--lat', type=float, default=None, help='纬度')
p.add_argument('--lon', type=float, default=None, help='经度')
p.add_argument('--tz', type=float, default=None, help='时区')
# 5. varga
p = sub.add_parser('varga', help='分盘计算')
_add_chart_args(p)
p.add_argument('--d9', action='store_true'); p.add_argument('--d10', action='store_true')
p.add_argument('--all', action='store_true')
# 6. celebrity
p = sub.add_parser('celebrity', help='名人案例查询')
p.add_argument('--name', default=None); p.add_argument('--limit', type=int, default=20)
# 7. db-stats
sub.add_parser('db-stats', help='验证数据库统计')
# 8. transit
p = sub.add_parser('transit', help='行星过境查询(Swiss Ephemeris实时计算)')
p.add_argument('--year', type=int, required=True); p.add_argument('--month', type=int, required=True)
p.add_argument('--day', type=int, default=15, help='指定日期(默认15日取月中代表)')
p.add_argument('--planet', default=None, help='目标行星,逗号分隔(默认全部,如:Jupiter,Saturn')
p.add_argument('--tz', type=float, default=None, help='时区')
p.add_argument('--node-mode', default='mean', choices=['mean', 'true'], help='Rahu/Ketu节点口径:mean=Mean Node(默认),true=True Node')
# 9. shadbala (v3.4新增)
p = sub.add_parser('shadbala', help='Shadbala六重力量计算(covered;外部绝对值校准前保留置信度上限)')
_add_chart_args(p)
p.add_argument('--table', action='store_true', help='以 ASCII 表格输出七曜力量汇总')
# 10. ashtakavarga (v3.4新增)
p = sub.add_parser('ashtakavarga', help='Ashtakavarga八分法计算')
_add_chart_args(p)
p.add_argument('--table', action='store_true', help='以 ASCII 表格输出 SAV 总表与 BAV 校验')
# 10b. kp (v6.9.10新增)
p = sub.add_parser('kp', help='KP Krishnamurti Paddhati 完整分析(Sublord+SubSub+ABCD Significator')
_add_chart_args(p)
# 10c. ashtakoot (v6.9.12新增)
p = sub.add_parser('ashtakoot', help='Ashtakoot 36点合婚 + Kuja Dosha 火星凶相分析')
# 男方参数
for prefix in ['m_', 'f_']:
label = '男方' if prefix == 'm_' else '女方'
p.add_argument(f'--{prefix}year', type=int, required=True, help=f'{label}出生年')
p.add_argument(f'--{prefix}month', type=int, required=True, help=f'{label}出生月')
p.add_argument(f'--{prefix}day', type=int, required=True, help=f'{label}出生日')
p.add_argument(f'--{prefix}hour', type=int, required=True, help=f'{label}出生时')
p.add_argument(f'--{prefix}minute', type=int, required=True, help=f'{label}出生分')
p.add_argument(f'--{prefix}lat', type=float, required=True, help=f'{label}出生纬度')
p.add_argument(f'--{prefix}lon', type=float, required=True, help=f'{label}出生经度')
p.add_argument(f'--{prefix}tz', type=float, default=0, help=f'{label}时区')
p.add_argument('--node-mode', default='mean', choices=['mean', 'true'])
p.add_argument('--table', action='store_true', help='以 ASCII 表格输出 8 Kuta 分数和总分')
# 11. memory (v3.4新增)
p = sub.add_parser('memory', help='Hermes记忆系统')
p.add_argument('--action', default='stats', choices=['store', 'search', 'context', 'stats'])
p.add_argument('--content', default=None, help='存储内容(store操作必填)')
p.add_argument('--query', default=None, help='搜索查询(search操作必填)')
p.add_argument('--tags', default=None, help='标签,逗号分隔')
p.add_argument('--importance', type=int, default=5, help='重要性 1-10')
p.add_argument('--limit', type=int, default=10, help='搜索结果数量')
# 12. validate (v3.5新增)
p = sub.add_parser('validate', help='R1-R10数学验证')
_add_chart_args(p)
# 13. audit (v3.5新增)
p = sub.add_parser('audit', help='P1-P12行星审计管线')
_add_chart_args(p)
# 14. report (v3.6新增)
p = sub.add_parser('report', help='MD→HTML报告生成(羊皮纸主题)')
p.add_argument('folder', help='包含MD文件的目录路径')
p.add_argument('--name', default='Client', help='客户姓名')
p.add_argument('--lagna', default='', help='上升星座')
p.add_argument('--gender', default='', help='性别')
p.add_argument('--status', default='', help='当前状态')
p.add_argument('--lang', default='cn', choices=['cn', 'en'], help='语言 (默认cn)')
p.add_argument('--output', default=None, help='输出HTML路径')
# 15. varga-full (v3.7新增 → v6.9.12 扩展变体/复合/自定义D-N)
p = sub.add_parser('varga-full', help='BPHS十六分盘+变体+复合+自定义D-N(2-300)')
_add_chart_args(p)
p.add_argument('--divisions', default=None, help='指定分盘,逗号分隔(如 D2,D9,D60),空=全部')
p.add_argument('--variant', default=None,
help='D2/D3变体名称。D2: parashara/pariveshta/parivritta/parivritta_trayodamsa/surya_chandra/ahoratra; D3: parashara/parivritta_trayodamsa/somaja/khara')
p.add_argument('--custom', type=int, default=None,
help='自定义D-N分盘,N=2-300(如 --custom 150')
p.add_argument('--composite', default=None,
help='复合分盘D-m×n,逗号分隔两个整数(如 --composite 9,12 = D108')
# 16. aspects (v3.7新增)
p = sub.add_parser('aspects', help='度数精确相位系统')
_add_chart_args(p)
# 17. jaimini (v3.7新增)
p = sub.add_parser('jaimini', help='Jaimini系统(Chara Karaka/Dasha/Karakamsha/Arudha/Special Lagnas')
_add_chart_args(p)
p.add_argument('--mode', default='all', choices=['all','karaka','dasha','karakamsha','arudha','special'], help='分析模式')
p.add_argument('--antardasha', action='store_true', help='Chara Dasha含Antardasha子周期(covered;仍需多系统确认)')
# 18. nakshatra-adv (v3.7新增 → v6.0.22 升级)
p = sub.add_parser('nakshatra-adv', help='高级Nakshatra分析(Tara/Chandra/Sub-Lord/综合)')
_add_chart_args(p)
p.add_argument('--mode', default='all',
choices=['all','detail','tara','chandra','combined','sublord','full'],
help='分析模式')
# 18.5 nakshatra-dasha (v6.0.22新增)
p = sub.add_parser('nakshatra-dasha', help='星宿大运推演(Ashtottari / Vimshottari Nakshatra-level / Transit Overlay')
_add_chart_args(p)
p.add_argument('--age', type=float, default=None, help='当前年龄(自动计算如果不提供)')
p.add_argument('--mode', default='all',
choices=['all','ashtottari','vimshottari','overlay'],
help='分析模式')
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DDoverlay/all模式,默认今天)')
# 18.6 nakshatra-full (v6.0.22新增)
p = sub.add_parser('nakshatra-full', help='综合星宿完整报告(本命 + 大运 + 过境)')
_add_chart_args(p)
p.add_argument('--age', type=float, default=None, help='当前年龄')
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(默认今天)')
# 19. argala (v3.7新增)
p = sub.add_parser('argala', help='Argala门闩系统')
_add_chart_args(p)
# 20. tajika (v3.7新增)
p = sub.add_parser('tajika', help='Tajika/Varshaphala年运盘')
_add_chart_args(p)
p.add_argument('--age', type=int, required=True, help='当前年龄')
p.add_argument('--mode', default='all', choices=['all','muntha','yearlord','mudda','tripataka'], help='分析模式')
# 21.5 solar-return (v6.0.18新增)
p = sub.add_parser('solar-return', help='太阳返照盘 Varshaphala 年运分析')
_add_chart_args(p)
p.add_argument('--target-year', type=int, required=True, help='目标年份(计算该年太阳返照)')
# 21.6 narayana-dasha (v6.0.20新增)
p = sub.add_parser('narayana-dasha', help='Narayana DashaRishi Dasha)星座大运分析')
_add_chart_args(p)
p.add_argument('--age', type=float, default=None, help='当前年龄(用于定位大运位置)')
# 21.7 muhurta (v6.0.21新增)
p = sub.add_parser('muhurta', help='Muhurta 择时分析(Panchanga 五要素)')
p.add_argument('--date', default=None, help='查询日期 YYYY-MM-DD(默认今天)')
p.add_argument('--activity', default=None,
choices=['marriage', 'business', 'travel', 'medical', 'education'],
help='指定活动类型(默认检查所有)')
p.add_argument('--scan-days', type=int, default=1, help='扫描天数(默认1天,最多30天)')
p.add_argument('--hour-from-sunrise', type=float, default=6.0,
help='从日出起算的小时数(默认6h约正午)')
# 21. synastry (v3.7新增)
p = sub.add_parser('synastry', help='合盘分析(Ashta Koota 36分制)')
p.add_argument('--moon1', type=float, required=True, help='Person1月亮黄经')
p.add_argument('--moon2', type=float, required=True, help='Person2月亮黄经')
p.add_argument('--mars1', type=float, default=None, help='Person1火星黄经')
p.add_argument('--mars2', type=float, default=None, help='Person2火星黄经')
p.add_argument('--asc1', type=float, default=None, help='Person1上升黄经')
p.add_argument('--asc2', type=float, default=None, help='Person2上升黄经')
p.add_argument('--gender1', default='M', help='Person1性别')
p.add_argument('--gender2', default='F', help='Person2性别')
# 22. full-reading (v3.7.1新增)
p = sub.add_parser('full-reading', help='全自动综合解盘(出生信息→全链路→完整报告)')
_add_chart_args(p)
p.add_argument('--age', type=int, default=None, help='当前年龄(不提供则自动计算)')
p.add_argument('--today', default=None, help='Dasha/Sandhi参考日期 YYYY-MM-DD(默认今天)')
p.add_argument('--transit-date', default=None, help='Transit真实过境参考日期 YYYY-MM-DD(默认跟随--today或今天)')
p.add_argument('--target-year', type=int, default=None, help='太阳返照盘目标年份(默认不计算 Varshaphala')
p.add_argument('--profile-stages', action='store_true', help='输出 full-reading 粗粒度阶段耗时,并在 summary 中附带 stage timings')
# 23. prashna (v3.9新增)
p = sub.add_parser('prashna', help='Prashna问事占星(提问时刻星盘+Arudha+Sphuta+Sahams')
p.add_argument('--datetime', required=True, help='提问时间 ISO-8601,例如 2026-07-12T12:00:00+08:00')
p.add_argument('--question-text', required=True, help='用户原始问事文本')
p.add_argument('--lat', type=float, required=True, help='纬度')
p.add_argument('--lon', type=float, required=True, help='经度')
p.add_argument('--timezone', required=True, help='UTC offset,例如 8 或 +08:00')
p.add_argument('--ayanamsa', default='lahiri')
p.add_argument('--node-mode', default='mean', choices=['mean', 'true'])
p.add_argument('--location-convention', default='wgs84', choices=['wgs84'])
p.add_argument('--mode', default='chart', choices=['chart','arudha','sphutas','sahams','lost-item','life','kunda'], help='分析模式')
# 24. double-transit-pac (v3.9新增)
p = sub.add_parser('double-transit-pac', help='Double Transit PAC + D9层(KN Rao完整实现)')
_add_chart_args(p)
p.add_argument('--date', required=True, help='过境日期 YYYY-MM-DD')
p.add_argument('--house', type=int, default=7, help='目标宫位(默认7=婚姻)')
# 25. transit-ll7l (v3.9新增)
p = sub.add_parser('transit-ll7l', help='Transit LL/7L连接+互换检测')
_add_chart_args(p)
p.add_argument('--date', required=True, help='过境日期 YYYY-MM-DD')
# 26. planetary-congregation (v3.9新增)
p = sub.add_parser('planetary-congregation', help='行星聚集检测(Lagna/7H+Transit')
_add_chart_args(p)
p.add_argument('--house', type=int, default=7, help='目标宫位')
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(可选)')
# 27. vivah-saham (v3.9新增)
p = sub.add_parser('vivah-saham', help='Vivah Saham计算+Transit激活')
_add_chart_args(p)
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(可选)')
# 29. bhava-chalit (v6.9.13新增)
p = sub.add_parser('bhava-chalit', help='Bhava Chalit 不等宫边界调整(Rashi vs Bhava 宫位对比)')
_add_chart_args(p)
p.add_argument('--house-system', default='sripati',
choices=['equal', 'placidus', 'porphyry', 'sripati', 'whole_sign', 'koch'],
help='宫位制(默认sripati')
p.add_argument('--mode', default='compare', choices=['compare', 'chart', 'boundaries'],
help='输出模式: compare=Rashi与Bhava对比, chart=Bhava宫位表, boundaries=宫位边界详情')
# 30. sudarshana (v6.9.14新增)
p = sub.add_parser('sudarshana', help='Sudarshana Chakra 三参考点盘分析(上升/月亮/太阳)')
_add_chart_args(p)
p.add_argument('--house', type=int, default=None, help='指定宫位(1-12)详细分析')
p.add_argument('--text', action='store_true', help='输出文本报告(默认JSON')
# 28. audit-capabilities (v6.0.3新增)
p = sub.add_parser('audit-capabilities', help='校验 technique registry 并输出能力覆盖审计')
p.add_argument('--registry', default=None, help='technique_registry.json 路径(默认 references/technique_registry.json')
p.add_argument('--mode', default='validate', choices=['validate', 'table'], help='validate=校验注册表;table=输出路由审计表')
p.add_argument('--route', default=None, help='table模式下的 route id,如 career_timing_strict')
args = parser.parse_args()
if hasattr(args, 'tz') and args.tz is None:
from timezone_utils import infer_timezone
from datetime import datetime
try:
if hasattr(args, 'year'):
dt = datetime(args.year, args.month, args.day, args.hour, args.minute)
else:
dt = datetime.utcnow()
except Exception:
dt = datetime.utcnow()
args.tz = infer_timezone(getattr(args, 'lat', 0), getattr(args, 'lon', 0), dt)
if args.tz is None:
args.tz = 8.0
if not args.command:
parser.print_help(); sys.exit(1)
# 应用 Ayanamsa 设置(v6.9.9 新增多系统支持)
ayanamsa_name = getattr(args, 'ayanamsa', 'lahiri')
if ayanamsa_name and ayanamsa_name != 'lahiri':
applied = _apply_ayanamsa(ayanamsa_name)
if not applied:
print(f"Warning: Ayanamsa '{ayanamsa_name}' not recognized, using default Lahiri", file=sys.stderr)
else:
print(f" [Ayanamsa] {ayanamsa_name}", file=sys.stderr)
cmds = { 'chart': cmd_chart, 'dasha': cmd_dasha, 'yoga': cmd_yoga, 'predict': cmd_predict,
'varga': cmd_varga, 'celebrity': cmd_celebrity, 'db-stats': cmd_db_stats, 'transit': cmd_transit,
'shadbala': cmd_shadbala, 'ashtakavarga': cmd_ashtakavarga, 'kp': cmd_kp,
'ashtakoot': cmd_ashtakoot, 'memory': cmd_memory,
'validate': cmd_validate, 'audit': cmd_audit, 'report': cmd_report,
'varga-full': cmd_varga_full, 'aspects': cmd_aspects, 'jaimini': cmd_jaimini,
'nakshatra-adv': cmd_nakshatra_adv, 'nakshatra-dasha': cmd_nakshatra_dasha,
'nakshatra-full': cmd_nakshatra_full, 'argala': cmd_argala, 'tajika': cmd_tajika,
'synastry': cmd_synastry, 'solar-return': cmd_solar_return,
'narayana-dasha': cmd_narayana_dasha, 'muhurta': cmd_muhurta,
'full-reading': cmd_full_reading, 'prashna': cmd_prashna,
'double-transit-pac': cmd_double_transit_pac,
'transit-ll7l': cmd_transit_ll7l, 'planetary-congregation': cmd_planetary_congregation,
'vivah-saham': cmd_vivah_saham,
'bhava-chalit': cmd_bhava_chalit,
'sudarshana': cmd_sudarshana}
if args.command == 'audit-capabilities':
from audit_capabilities import build_audit_table, load_registry, validate_registry
registry = load_registry(args.registry) if args.registry else load_registry()
result = validate_registry(registry) if args.mode == 'validate' else build_audit_table(registry, args.route)
output_json(result)
sys.exit(0 if result.get('valid', True) else 1)
result = cmds[args.command](args)
if args.command in {'predict', 'full-reading'} and isinstance(result, dict):
from timing_precision_contract import build_timing_precision_contract
result['timing_precision_contract'] = build_timing_precision_contract(result.get('timing'))
if getattr(args, 'table', False):
output_table(args.command, result)
else:
output_json(result)
if __name__ == '__main__':
main()