diff --git a/CHANGELOG.md b/CHANGELOG.md index 03f7776d..dd79ef4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # 印度占星 Skill 更新日志 +## v6.0.7-node-mode-arbitration(2026-06-03)— Rahu/Ketu 节点口径仲裁与参数冻结 + +> **触发原因**:可信度 benchmark 第四轮确认第三轮 PyJHora 中 Rahu/Ketu 的剩余差异主要来自 Mean Node / True Node 口径差异,而非 D9/D10 计算 bug。 + +### 修复内容 + +- `scripts/jyotish_engine.py` 的出生数据类子命令新增 `--node-mode mean|true` 参数,默认 `mean`,可选 `true` 用于对齐 PyJHora 默认口径。 +- `compute_chart_data()` 输出 `birth_info.node_mode` 与 `node_mode_note`,把 Rahu/Ketu 节点模式纳入参数冻结证据。 +- `SKILL.md` 新增“Rahu/Ketu 节点口径冻结”强制规则,要求 benchmark 与解盘输出显式声明 Mean/True Node。 +- 第四轮报告确认:当前 skill vs Swiss Mean Node 80/80 匹配;vs Swiss True Node 与 PyJHora 默认均为 54/80,差异模式完全一致。 + +--- + ## v6.0.6-d10-benchmark-fix(2026-06-03)— PyJHora benchmark 暴露的 D10 口径修复 > **触发原因**:可信度 benchmark 第三轮接入 PyJHora 后,D10 出现系统性错配;交叉查证 D10 规则为“奇数星座从本宫起算,偶数星座从第九宫起算(含本宫计数)”。 diff --git a/SKILL.md b/SKILL.md index 19d67408..7a5c4362 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: jyotish-vedic-astrology -version: 6.0.6 +version: 6.0.7 description: 印度占星(Jyotish)专业解盘与推运系统。核心能力:PDF星盘输入→严谨解盘→精确推运应期输出。触发词:印度占星、吠陀占星、Jyotish、解盘、推运、星盘分析、Dasha、Transit、Nakshatra、Yoga、出生时间矫正、PDF星盘、读取PDF、分析PDF星盘、现代解读、误判纠错、Varga分盘、综合分析、过境分析、合盘、婚姻匹配、年运盘、Prashna、Argala、Jaimini、KP系统、Shadbala、Ashtakavarga、HTML报告、深度解盘。 --- @@ -10,7 +10,7 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力 > **严格路由**:`references/strict-workflow-router.md`(⭐涉及事业/婚恋/财务/应期/技法验证时必须优先读取) > **覆盖矩阵**:`references/technique-capability-matrix.md`(⭐判断技法 covered/partial/missing 时必须参考) > **机器注册表**:`references/technique_registry.json` + `scripts/audit_capabilities.py`(⭐用于自动审计与CI门禁) -> **版本**:v6.0.6-d10-benchmark-fix | **详细变更**:`CHANGELOG.md` +> **版本**:v6.0.7-node-mode-arbitration | **详细变更**:`CHANGELOG.md` --- @@ -88,6 +88,15 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力 → 完整规范:`references/transit-actionable-output-guide.md` +### Rahu/Ketu 节点口径冻结(v6.0.7-node-mode) + +**所有 benchmark 与解盘输出必须显式声明 Rahu/Ketu 使用 Mean Node 还是 True Node。** + +- 当前 skill 默认:`--node-mode mean`(Swiss Ephemeris Mean Node)。 +- 可选:`--node-mode true`(Swiss Ephemeris True Node,用于对齐 PyJHora 默认口径)。 +- PyJHora 4.8.6 的 `rasi_chart()` 默认使用 True Node;第三轮 benchmark 的 Rahu/Ketu 差异已由第四轮仲裁确认为 Mean/True Node 口径差异,不应再误判为 D9/D10 计算 bug。 +- 输出 `birth_info.node_mode` 与 `node_mode_note` 必须保留,作为参数冻结证据。 + --- ## 核心能力速查 @@ -255,9 +264,9 @@ $PYTHON $SCRIPT <子命令> [参数] --- -**版本**:v6.0.6-d10-benchmark-fix +**版本**:v6.0.7-node-mode-arbitration **创建日期**:2026-04-20 -**最后更新**:2026-06-03(v6.0.6 修正 D10 偶数星座起算与非D9分盘度数缩放口径) +**最后更新**:2026-06-03(v6.0.7 新增 Rahu/Ketu Mean/True Node 口径冻结与 `--node-mode` 参数) --- diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 7b9c7bb4..6e26d856 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -242,7 +242,8 @@ def _get_dignity_level(planet, sign, deg_in_sign=None): return 'ENEMY' return 'NEUTRAL' PLANET_CN = {"Ketu": "南交点Ketu", "Venus": "金星Venus", "Sun": "太阳Sun", "Moon": "月亮Moon", "Mars": "火星Mars", "Rahu": "北交点Rahu", "Jupiter": "木星Jupiter", "Saturn": "土星Saturn", "Mercury": "水星Mercury"} -PLANETS_SWE = {'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} if HAS_SWE else {} +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 output_json(data): @@ -260,13 +261,14 @@ def _add_chart_args(p): p.add_argument('--lat', type=float, required=True) p.add_argument('--lon', type=float, required=True) p.add_argument('--tz', type=float, default=0) + p.add_argument('--node-mode', default='mean', choices=['mean', 'true'], help='Rahu/Ketu节点口径:mean=Mean Node(默认,JHora常用/Swiss direct baseline),true=True Node(PyJHora默认)') # ============================================================================ # 公共星盘计算(供 chart/shadbala/ashtakavarga 共用,v3.4提取) # ============================================================================ -def compute_chart_data(year, month, day, hour, minute, lat, lon, tz): - """计算星盘核心数据,返回 (result_dict, asc_idx, jd, ayanamsa)""" +def compute_chart_data(year, month, day, hour, minute, lat, lon, tz, node_mode='mean'): + """计算星盘核心数据,返回 (result_dict, asc_idx, jd, ayanamsa)。node_mode: mean|true。""" if not HAS_SWE: return None, None, None, None swe.set_ephe_path('') @@ -274,10 +276,15 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, 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 = swe.TRUE_NODE if node_mode == 'true' else swe.MEAN_NODE result = {"birth_info": { "date": f"{year}-{month:02d}-{day:02d}", "time": f"{hour:02d}:{minute:02d}", "tz": f"UTC{'+' if tz >= 0 else ''}{tz}", "lat": lat, "lon": lon, - "julian_day": round(jd, 6), "ayanamsa": round(ayanamsa, 4) + "julian_day": round(jd, 6), "ayanamsa": round(ayanamsa, 4), + "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') @@ -296,7 +303,8 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, tz): "lord": SIGN_LORDS[SIGNS[si]]} nak_span = 360.0 / 27 - for pname, pid in PLANETS_SWE.items(): + 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] @@ -336,7 +344,7 @@ def compute_chart_data(year, month, day, hour, minute, lat, lon, tz): # 1. 星盘计算 # ============================================================================ def cmd_chart(args): - result, asc_idx, jd, ayanamsa = compute_chart_data(args.year, args.month, args.day, args.hour, args.minute, args.lat, args.lon, args.tz) + result, asc_idx, jd, ayanamsa = compute_chart_data(args.year, args.month, args.day, args.hour, args.minute, args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if result is None: return {"error": "swisseph未安装"} # v3.5: --validate 触发 R1-R10 校验 @@ -872,7 +880,7 @@ def cmd_predict(args): if getattr(args, 'past_verify', False) and args.year: chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} return _past_event_verify(chart, asc_idx, args) @@ -1435,7 +1443,7 @@ def cmd_double_transit_pac(args): # 1. 计算本命星盘 chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} @@ -1679,7 +1687,7 @@ def cmd_transit_ll7l(args): """Transit LL/7L 连接 + 互换检测""" chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} @@ -1764,7 +1772,7 @@ def cmd_planetary_congregation(args): """行星聚集检测""" chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} @@ -1844,7 +1852,7 @@ def cmd_vivah_saham(args): """Vivah Saham 计算 + Transit 激活""" chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} @@ -1908,7 +1916,7 @@ def cmd_vivah_saham(args): def cmd_shadbala(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} try: @@ -1930,7 +1938,7 @@ def cmd_shadbala(args): def cmd_ashtakavarga(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} try: @@ -1990,7 +1998,7 @@ def cmd_memory(args): def cmd_validate(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} try: @@ -2119,7 +2127,7 @@ def cmd_audit(args): """P1-P12 行星审计:调用 chart→shadbala→ashtakavarga→yoga,输出统一审计报告""" chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} @@ -2384,7 +2392,7 @@ def cmd_report(args): def cmd_varga_full(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} try: @@ -2405,7 +2413,7 @@ def cmd_varga_full(args): def cmd_aspects(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} try: @@ -2428,7 +2436,7 @@ def cmd_aspects(args): def cmd_jaimini(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} try: @@ -2475,7 +2483,7 @@ def cmd_jaimini(args): def cmd_nakshatra_adv(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} try: @@ -2513,7 +2521,7 @@ def cmd_nakshatra_adv(args): def cmd_argala(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} try: @@ -2537,7 +2545,7 @@ def cmd_argala(args): def cmd_tajika(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装"} try: @@ -3065,7 +3073,7 @@ def cmd_full_reading(args): # ── Step 1: 核心星盘 ── chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, - args.lat, args.lon, args.tz) + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) if chart is None: return {"error": "swisseph未安装,无法计算星盘"}