diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 7c9eef20..601a5fd3 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -16484,3 +16484,21 @@ - 相关记录:BUG-1160、BUG-1161、BUG-1218。 - 复发自:无 - 修复版本:待发布 + +## BUG-1222 | 年运强度层的 Panchavargiya 一列与年主选法不是同一套分数 + +- 状态:resolved(分支 `codex/astrologer-rulings-batch4-20261003`,未推送;待验收) +- 首次发现 / 最近更新:2026-10-03 / 2026-10-03 +- 来源:第三批进度记录「下轮问占星师」第 10 条(产品问题);产品负责人 2026-10-03 决定;任务书 `TASK-astrologer-rulings-batch4-20261003` T2。 +- 影响面:`scripts/tajika.calc_tajika_strength_layers` → `solar_return_full_report` 的 `tajika_strength`(`/api/annual`、`/api/tajika` 的 `report.tajika_strength`,`solar-return` 命令行 JSON)、`varshaphala` 的 `tajika_strength`。`scripts/solar_return.py` 未改。 +- 现象:BUG-1217 起年主改用 Tajika 正式五分法比强弱,同一份年运结果里的「Harsha / Panchavargiya」强度层 Panchavargiya 一列仍是 `blocked`(`unified_varga_core_and_golden_oracle_parity_required`),函数里还留着 D1/D2/D3/D9/D12 庙旺近似的旧实现(`return` 之后不可达)。两处口径不一致。 +- 实际情况(开工核对,与任务书事故实证 2 的出入):本仓没有渲染出来的「强度表」——参考版 / 读者版报告与年运包(`annual_tajika_pack`)都不带 `tajika_strength`,前端没有使用方;该层只出现在上面列的 API / 命令行 JSON 里。近似分没有上表,上表的是 `blocked`。 +- 根因:第二、三批只改了年主选法(BUG-1214 / 1217),强度层停在更早的「等 oracle 对齐」阻塞状态;上游 `c7117f92` 这里已改用五分法。 +- 决策(产品负责人 2026-10-03):报告年运强度表统一改用 Tajika 正式五分法,不再显示庙旺近似分。 +- 修复:`calc_tajika_strength_layers` 的 `panchavargiya_bala` 每星取 `_calc_native_panchavargiya_bala`(与年主选法同一函数、同一输入 `planet_lons`):`status: usable`、`engine: tajika_native_five_fold`、`label`「Tajika 五分力量」/ `label_en`「Tajika five-fold strength」、`score`(满分 20)、`raw_total`(满分 80)、五个分量(graha / uchcha / hudda / drekkana / navamsha)、`component_trace`、规则来源;层级字段 `panchavargiya_method` / `_en`。年盘缺星时该列 blocked 并写原因,不退回近似分。删掉函数里不可达的庙旺近似旧实现。Harsha Bala 不变。Harsha + Panchavargiya 合成强度没有裁定,仍 blocked(原因改为 `combined_tajika_strength_not_ruled`)。 +- 仍用庙旺近似的地方:`tajika._resolve_year_lord_panchavargiya_score` 在 `native_scores` 未传时(上游原行为,无产品调用方);`_calc_panchavargiya_bala_for_planet` 本身保留供该路径与「近似函数被调用即失败」的测试使用。`_strength_interpretation` / `_strength_headline` 原只被不可达代码调用,现无调用方,未删。 +- 验证:新增 `tests/test_annual_strength_five_fold.py`(3 张虚构盘 × 2025 / 2026:年主每个候选的分数、原始分、五个分量与强度层同一颗星逐项相同;强度层没有近似分的分盘字段、满分 20、score = 原始 / 4;Harsha 形状不变);`tests/test_tajika.py` 改写一条、新增两条(近似函数被调用即失败、缺星 blocked);`tests/test_api_server_security.py` 年运端点一条。9 位公开名人 2026 年强度层前后对比见进度记录:年主、候选分、Harsha 9/9 不变,Panchavargiya 列由 blocked 变为五分法分数且与候选分逐项相同。 +- 防复发:上述测试。 +- 相关记录:BUG-1217、BUG-1214、BUG-1219。 +- 复发自:无 +- 修复版本:待发布 diff --git a/scripts/tajika.py b/scripts/tajika.py index f1db4076..e976d9ab 100644 --- a/scripts/tajika.py +++ b/scripts/tajika.py @@ -533,12 +533,24 @@ def calc_tri_pataka(planet_lons: Dict[str, float], } +TAJIKA_FIVE_FOLD_LABEL = 'Tajika 五分力量' +TAJIKA_FIVE_FOLD_LABEL_EN = 'Tajika five-fold strength' + + def calc_tajika_strength_layers( planet_lons: Dict[str, float], asc_lon: float = 0.0, year_lord: Optional[str] = None, ) -> Dict: - """Block the legacy private-Varga strength proxy pending parity evidence.""" + """Annual Tajika strength: Harsha Bala and the Tajika five-fold Panchavargiya. + + BUG-1222 (product 2026-10-03): the Panchavargiya column is the same + Tajika five-fold strength the year-lord selection compares + (``_calc_native_panchavargiya_bala``, BUG-1217): five components and a + score out of 20 (raw out of 80 / 4). The D1/D2/D3/D9/D12 dignity proxy is + no longer on this layer. Harsha Bala is unchanged. The combined + Harsha + Panchavargiya figure has no ruling yet and stays blocked. + """ normalized = { planet: float(planet_lons[planet]) % 360 for planet in CLASSICAL_PLANETS @@ -548,19 +560,42 @@ def calc_tajika_strength_layers( planet: _calc_harsha_bala_for_planet(planet, lon, asc_lon, year_lord) for planet, lon in normalized.items() } - panchavargiya_bala = { - planet: { - 'status': 'blocked', - 'reason': 'unified_varga_core_and_golden_oracle_parity_required', + native = _calc_native_panchavargiya_bala(normalized) if normalized else {} + panchavargiya_bala = {} + for planet in normalized: + row = native.get(planet) or {} + if row.get('status') != 'native_rule_backed': + panchavargiya_bala[planet] = { + 'status': 'blocked', + 'reason': row.get('reason') or 'tajika_five_fold_panchavargiya_unavailable', + 'label': TAJIKA_FIVE_FOLD_LABEL, + 'label_en': TAJIKA_FIVE_FOLD_LABEL_EN, + } + continue + panchavargiya_bala[planet] = { + 'status': 'usable', + 'engine': 'tajika_native_five_fold', + 'label': TAJIKA_FIVE_FOLD_LABEL, + 'label_en': TAJIKA_FIVE_FOLD_LABEL_EN, + 'score': row['score'], + 'max_score': 20.0, + 'raw_total': row['raw_total'], + 'raw_max_score': 80.0, + 'components': dict(row['components']), + 'component_trace': row['component_trace'], + 'rule_source': row['rule_source'], } - for planet in normalized - } + panchavargiya_ready = bool(panchavargiya_bala) and all( + row['status'] == 'usable' for row in panchavargiya_bala.values() + ) return { 'status': 'partial', 'method': 'Tajika Harsha/Panchavargiya Bala', - 'reason': 'panchavargiya_requires_unified_varga_core_and_golden_oracle_parity', - 'usable_layers': ['Harsha Bala'], - 'blocked_layers': ['Panchavargiya Bala', 'combined Tajika strength'], + 'panchavargiya_method': TAJIKA_FIVE_FOLD_LABEL, + 'panchavargiya_method_en': TAJIKA_FIVE_FOLD_LABEL_EN, + 'reason': 'combined_tajika_strength_not_ruled', + 'usable_layers': ['Harsha Bala'] + (['Panchavargiya Bala'] if panchavargiya_ready else []), + 'blocked_layers': ([] if panchavargiya_ready else ['Panchavargiya Bala']) + ['combined Tajika strength'], 'available_planets': len(normalized), 'harsha_bala': { planet: {**data, 'status': 'usable'} @@ -570,7 +605,7 @@ def calc_tajika_strength_layers( 'combined_strength': { planet: { 'status': 'blocked', - 'reason': 'panchavargiya_component_unverified', + 'reason': 'combined_tajika_strength_not_ruled', 'score': harsha_bala[planet]['score'], 'max_score': harsha_bala[planet]['max_score'], 'grade': 'blocked', @@ -579,62 +614,7 @@ def calc_tajika_strength_layers( for planet in normalized }, 'summary': { - 'next_action': 'Import unified varga-full output and external golden-oracle evidence before rendering Tajika strength.', - }, - } - - # Legacy local Varga proxy retained below only for source history. - normalized = { - planet: float(planet_lons[planet]) % 360 - for planet in CLASSICAL_PLANETS - if planet in planet_lons and _is_number(planet_lons[planet]) - } - harsha_bala = {} - panchavargiya_bala = {} - combined_strength = {} - - for planet, lon in normalized.items(): - harsha = _calc_harsha_bala_for_planet(planet, lon, asc_lon, year_lord) - panchavargiya = _calc_panchavargiya_bala_for_planet(planet, lon) - total = round(harsha['score'] + panchavargiya['score'], 2) - max_score = harsha['max_score'] + panchavargiya['max_score'] - grade = _strength_grade(total, max_score) - harsha_bala[planet] = harsha - panchavargiya_bala[planet] = panchavargiya - combined_strength[planet] = { - 'score': total, - 'max_score': max_score, - 'grade': grade, - 'components': { - 'harsha_bala': harsha['score'], - 'panchavargiya_bala': panchavargiya['score'], - }, - 'interpretation': _strength_interpretation(planet, grade), - } - - ranked = sorted( - ( - {'planet': planet, **data} - for planet, data in combined_strength.items() - ), - key=lambda item: item['score'], - reverse=True, - ) - - return { - 'method': 'Tajika Harsha/Panchavargiya Bala', - 'source': '本地 Tajika 强度模型:Harsha Bala + Panchavargiya 五分盘尊贵度代理,用于年度盘证据链与用户端排序。', - 'available_planets': len(normalized), - 'year_lord': year_lord or '', - 'ascendant_longitude': round(float(asc_lon or 0) % 360, 4), - 'harsha_bala': harsha_bala, - 'panchavargiya_bala': panchavargiya_bala, - 'combined_strength': combined_strength, - 'summary': { - 'strongest_planets': ranked[:3], - 'weakest_planets': list(reversed(ranked[-3:])) if ranked else [], - 'headline': _strength_headline(ranked), - 'next_action': '用最强星解释年度可主动推进的主题,用最弱星标注需要 Dasha/Transit 二次确认的风险点。', + 'next_action': 'Read Harsha Bala and the Tajika five-fold Panchavargiya side by side; no combined Tajika strength until it is ruled.', }, } diff --git a/tests/test_annual_strength_five_fold.py b/tests/test_annual_strength_five_fold.py new file mode 100644 index 00000000..6cbabf7a --- /dev/null +++ b/tests/test_annual_strength_five_fold.py @@ -0,0 +1,68 @@ +"""BUG-1222: the annual strength layer and the year-lord selection use one score. + +Product 2026-10-03 (TASK-astrologer-rulings-batch4-20261003 T2): the annual +「Harsha / Panchavargiya」 strength layer's Panchavargiya column is the Tajika +five-fold strength (`_calc_native_panchavargiya_bala`) the year-lord +selection compares (BUG-1217), not the D1/D2/D3/D9/D12 dignity proxy. +Fictional birth data only. +""" + +from __future__ import annotations + +import os +import sys + +import pytest + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path[:0] = [ROOT, os.path.join(ROOT, "scripts")] + +import solar_return # noqa: E402 + +pytest.importorskip("swisseph") + +# Fictional births (no real person); several target years so the shortlist +# branches differ. +BIRTHS = ( + (1990, 6, 15, 12, 0, 28.6, 77.2, 5.5), + (1975, 11, 2, 4, 30, 40.7, -74.0, -5.0), + (2001, 2, 20, 21, 10, 51.5, -0.1, 0.0), +) + + +@pytest.mark.parametrize("birth", BIRTHS) +@pytest.mark.parametrize("target_year", (2025, 2026)) +def test_strength_table_and_year_lord_share_the_five_fold_score(birth, target_year): + report = solar_return.solar_return_full_report(*birth, target_year, ayanamsa_name="raman") + strength = report["tajika_strength"] + year_lord = report["year_lord"] + candidates = year_lord.get("candidates") or [] + assert candidates, year_lord + assert year_lord["panchavargiya_engine"] == "tajika_native_five_fold" + for candidate in candidates: + row = strength["panchavargiya_bala"][candidate["planet"]] + assert row["status"] == "usable" + assert row["engine"] == "tajika_native_five_fold" + assert row["score"] == candidate["panchavargiya_score"], candidate["planet"] + assert row["raw_total"] == candidate["panchavargiya_raw_total"], candidate["planet"] + assert row["components"] == candidate["panchavargiya_components"], candidate["planet"] + lord = year_lord["year_lord"] + chosen = next(item for item in candidates if item["planet"] == lord) + assert strength["panchavargiya_bala"][lord]["score"] == chosen["panchavargiya_score"] + + +def test_strength_layer_has_no_dignity_proxy_fields(): + report = solar_return.solar_return_full_report(*BIRTHS[0], 2026, ayanamsa_name="raman") + strength = report["tajika_strength"] + assert strength["panchavargiya_method"] == "Tajika 五分力量" + assert strength["panchavargiya_method_en"] == "Tajika five-fold strength" + for row in strength["panchavargiya_bala"].values(): + # The proxy's D-chart components (rasi / hora / dwadasamsa …) are gone. + assert set(row["components"]) == {"graha_bala", "uchcha_bala", "hudda_bala", "drekkana_bala", "navamsha_bala"} + assert row["max_score"] == 20.0 + assert 0 <= row["score"] <= 20.0 + assert row["score"] == pytest.approx(row["raw_total"] / 4, abs=0.01) + # Harsha Bala keeps its own shape and scale. + for row in strength["harsha_bala"].values(): + assert row["status"] == "usable" + assert row["max_score"] == 15.0 diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py index fbb84082..d181c887 100644 --- a/tests/test_api_server_security.py +++ b/tests/test_api_server_security.py @@ -3244,7 +3244,11 @@ def test_annual_endpoint_returns_varshaphala_report() -> None: strength = result['report']['tajika_strength'] assert strength['status'] == 'partial' assert strength['method'] == 'Tajika Harsha/Panchavargiya Bala' - assert strength['usable_layers'] == ['Harsha Bala'] + # 原值: strength['usable_layers'] == ['Harsha Bala'] + # 新值: ['Harsha Bala', 'Panchavargiya Bala'],Panchavargiya 为 Tajika 五分力量 + # 原因: 产品 2026-10-03 决定年运强度表统一用 Tajika 正式五分法(BUG-1222) + assert strength['usable_layers'] == ['Harsha Bala', 'Panchavargiya Bala'] + assert strength['panchavargiya_method'] == 'Tajika 五分力量' assert 'harsha_bala' in strength assert 'panchavargiya_bala' in strength assert 'strongest_planets' not in strength['summary'] diff --git a/tests/test_tajika.py b/tests/test_tajika.py index 2f10052b..d21667d9 100644 --- a/tests/test_tajika.py +++ b/tests/test_tajika.py @@ -13,6 +13,7 @@ from tajika import ( calc_muntha, calc_year_lord, calc_mudda_dasha, calc_tri_pataka, calc_tajika_yogas, detect_tajika_yogas, detect_vedha, calc_all_sahams, calc_tajika_strength_layers, _is_faster, + _calc_native_panchavargiya_bala, SAHAM_RULES_PATH, SIGNS, SIGN_LORDS, ) @@ -109,6 +110,10 @@ class TestTriPataka: # ── Harsha / Panchavargiya Bala Tests ───────────────────────────── class TestTajikaStrengthLayers: + # 原值: Panchavargiya 列 blocked('Panchavargiya Bala' in blocked_layers;usable_layers == ['Harsha Bala'];每星 status == 'blocked') + # 新值: Panchavargiya 列由 Tajika 五分力量驱动(usable_layers == ['Harsha Bala', 'Panchavargiya Bala'];每星 status == 'usable'、 + # 分数等于 _calc_native_panchavargiya_bala、满分 20、五个分量、中英标签);合成强度仍 blocked + # 原因: 产品 2026-10-03 决定年运强度表统一用 Tajika 正式五分法(BUG-1222,关联 BUG-1217) def test_harsha_and_panchavargiya_strength_layers(self): planet_lons = { 'Sun': 10.0, @@ -124,18 +129,51 @@ class TestTajikaStrengthLayers: assert result['status'] == 'partial' assert result['method'] == 'Tajika Harsha/Panchavargiya Bala' - assert 'Panchavargiya Bala' in result['blocked_layers'] - assert result['usable_layers'] == ['Harsha Bala'] + assert result['blocked_layers'] == ['combined Tajika strength'] + assert result['usable_layers'] == ['Harsha Bala', 'Panchavargiya Bala'] + assert result['panchavargiya_method'] == 'Tajika 五分力量' + assert result['panchavargiya_method_en'] == 'Tajika five-fold strength' assert result['summary']['next_action'] + native = _calc_native_panchavargiya_bala(planet_lons) for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'): assert planet in result['harsha_bala'] assert result['harsha_bala'][planet]['status'] == 'usable' - assert planet in result['panchavargiya_bala'] - assert result['panchavargiya_bala'][planet]['status'] == 'blocked' + row = result['panchavargiya_bala'][planet] + assert row['status'] == 'usable' + assert row['engine'] == 'tajika_native_five_fold' + assert (row['label'], row['label_en']) == ('Tajika 五分力量', 'Tajika five-fold strength') + assert row['score'] == native[planet]['score'] + assert row['raw_total'] == native[planet]['raw_total'] + assert row['max_score'] == 20.0 and row['raw_max_score'] == 80.0 + assert set(row['components']) == {'graha_bala', 'uchcha_bala', 'hudda_bala', 'drekkana_bala', 'navamsha_bala'} + assert row['components'] == native[planet]['components'] + assert 0 <= row['score'] <= 20.0 assert result['combined_strength'][planet]['status'] == 'blocked' assert result['combined_strength'][planet]['max_score'] > 0 + def test_dignity_proxy_is_not_on_the_strength_layer(self, monkeypatch): + """BUG-1222: the D1/D2/D3/D9/D12 dignity proxy is never called for the table.""" + import tajika as tajika_module + + def boom(*_args, **_kwargs): + raise AssertionError('dignity proxy called') + + monkeypatch.setattr(tajika_module, '_calc_panchavargiya_bala_for_planet', boom) + planet_lons = {'Sun': 10.0, 'Moon': 45.0, 'Mars': 92.0, 'Mercury': 152.0, + 'Jupiter': 244.0, 'Venus': 330.0, 'Saturn': 300.0} + result = tajika_module.calc_tajika_strength_layers(planet_lons, asc_lon=15.0, year_lord='Jupiter') + assert all(row['status'] == 'usable' for row in result['panchavargiya_bala'].values()) + + def test_missing_planet_blocks_the_five_fold_column(self): + planet_lons = {'Sun': 10.0, 'Moon': 45.0, 'Mars': 92.0} + result = calc_tajika_strength_layers(planet_lons, asc_lon=15.0) + assert 'Panchavargiya Bala' in result['blocked_layers'] + assert result['usable_layers'] == ['Harsha Bala'] + for row in result['panchavargiya_bala'].values(): + assert row['status'] == 'blocked' + assert 'score' not in row + # ── Tajika Yogas Tests ─────────────────────────────────────────────