fix(engine): D9 double-transit targets check the D9 signs they name (BUG-1060)
`cmd_double_transit_pac` placed the `D9_{N}宫` target on the D9 lagna sign
and the `D9_{lord}(宫主)` target on that planet's D1 longitude. Both now sit
on D9 signs (D9 house-N sign; the lord's navamsa sign), built in one
testable helper. Target names, D1 and Chandra Lagna layers are unchanged
(pre-fix golden, byte-identical). Evidence-card golden regenerated with the
capture script (one public chart: double-transit conclusions 9 -> 10).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5671039af0
commit
f6fa367f2b
File diff suppressed because one or more lines are too long
+29
-18
@@ -13020,6 +13020,33 @@ def _check_pac(planet_name, planet_lon, target_lon, asc_idx):
|
||||
return results
|
||||
|
||||
|
||||
def _double_transit_d9_targets(asc_deg, natal, event_house, event_lord, ll_name):
|
||||
"""Double Transit PAC 的 D9 层目标:返回 (D9 上升索引, {目标名: 参与 PAC 的经度})。
|
||||
|
||||
D9 层把过境木星/土星按 D1 星座叠到 D9 盘上、宫位从 D9 上升数,所以每个目标
|
||||
都取 D9 盘上的星座中点:
|
||||
- ``D9_{N}宫(<星座>)``:D9 第 N 宫星座(BUG-1060:此前误用 D9 上升星座);
|
||||
- ``D9_{宫主}(宫主)``:D9 第 N 宫宫主在 D9 的星座(BUG-1060:此前误用该星 D1 度数);
|
||||
- ``{D1 第 N 宫主}_D9(<星座>)`` / ``{上升主}_D9(<星座>)``:该星在 D9 的星座(KN Rao「宫主 D9 星座」)。
|
||||
"""
|
||||
d9_asc_idx = _navamsa_idx(asc_deg)
|
||||
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]
|
||||
|
||||
def d9_si(planet):
|
||||
return _navamsa_idx(natal.get(planet, {}).get('degree', 0))
|
||||
|
||||
event_lord_d9_si = d9_si(event_lord)
|
||||
ll_d9_si = d9_si(ll_name)
|
||||
return d9_asc_idx, {
|
||||
f'D9_{event_house}宫({d9_event_sign})': d9_event_si * 30 + 15,
|
||||
f'D9_{d9_event_lord}(宫主)': d9_si(d9_event_lord) * 30 + 15,
|
||||
f'{event_lord}_D9({SIGNS[event_lord_d9_si]})': event_lord_d9_si * 30 + 15,
|
||||
f'{ll_name}_D9({SIGNS[ll_d9_si]})': ll_d9_si * 30 + 15,
|
||||
}
|
||||
|
||||
|
||||
def cmd_double_transit_pac(args):
|
||||
"""Double Transit PAC + D9 层计算"""
|
||||
# 1. 计算本命星盘
|
||||
@@ -13074,26 +13101,10 @@ def cmd_double_transit_pac(args):
|
||||
}
|
||||
|
||||
# 4. D9 层敏感点
|
||||
d9_asc_idx = _navamsa_idx(asc_deg)
|
||||
d9_asc_idx, d9_targets = _double_transit_d9_targets(asc_deg, natal, event_house, event_lord, ll_name)
|
||||
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,
|
||||
}
|
||||
event_lord_d9_sign = SIGNS[_navamsa_idx(event_lord_lon)]
|
||||
|
||||
# 5. PAC 检查
|
||||
results = {
|
||||
|
||||
@@ -8,7 +8,13 @@ default twelve-month horizon. External VedAstro is not called (same stand-in
|
||||
as the research runner). Each response is trimmed by key only: every kept
|
||||
value is the engine's own value, unchanged.
|
||||
|
||||
PYTHONHASHSEED=0 python scripts/research/capture_consult_evidence_card_golden.py
|
||||
JYOTISH_API_CHART_CACHE_TTL_SECONDS=0 PYTHONHASHSEED=0 \
|
||||
python scripts/research/capture_consult_evidence_card_golden.py
|
||||
|
||||
The chart cache (`scratch/local/api_chart_cache`, 15-minute TTL) stores
|
||||
responses with sorted keys, so a warm cache changes dict and list order (not
|
||||
values) between runs; TTL 0 keeps every request cold and the output
|
||||
byte-reproducible (BUG-1060).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -102,6 +102,8 @@ CORE_PYTEST_TARGETS = [
|
||||
"tests/test_punarphoo_observation.py",
|
||||
# Native technique layers exposed to the web consultation (evidence card v2).
|
||||
"tests/test_consultation_native_layers.py",
|
||||
# BUG-1060: Double Transit PAC D9 targets use the D9 signs their labels name.
|
||||
"tests/test_double_transit_d9_targets.py",
|
||||
"tests/test_relationship_event_class_evidence.py",
|
||||
"tests/test_thematic_dasha_info_source.py",
|
||||
"tests/test_report_orchestrator_marriage_timing.py",
|
||||
|
||||
+2440
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,176 @@
|
||||
"""BUG-1060: Double Transit PAC D9 targets check the D9 signs their labels name.
|
||||
|
||||
Real engine runs on the three public AA charts of the evidence-card research
|
||||
(`consult_evidence_card_lib.load_public_charts`, raman ayanamsa, mean nodes,
|
||||
reference date fixed in the library). The D1 and Chandra Lagna layers are
|
||||
compared with a golden captured from the engine before the fix (base
|
||||
5671039a); the fix only touches the D9 layer.
|
||||
|
||||
Regenerate the golden (only when the D1 / CL layer is meant to change):
|
||||
|
||||
PYTHONHASHSEED=0 python tests/test_double_transit_d9_targets.py --capture
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
for path in (ROOT, ROOT / "scripts", ROOT / "scripts" / "research"):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
import jyotish_engine as engine # noqa: E402
|
||||
from consult_evidence_card_lib import load_public_charts # noqa: E402
|
||||
|
||||
GOLDEN = ROOT / "tests" / "fixtures" / "double_transit_d1_cl_golden.json"
|
||||
HOUSES = range(1, 13)
|
||||
|
||||
|
||||
def _args(chart: dict, house: int) -> SimpleNamespace:
|
||||
body = chart["body"]
|
||||
return SimpleNamespace(
|
||||
year=body["year"], month=body["month"], day=body["day"], hour=body["hour"], minute=body["minute"],
|
||||
second=0, lat=body["lat"], lon=body["lon"], tz=float(body["tz"]), node_mode=body["node_mode"],
|
||||
ayanamsa=body["ayanamsa"], date=body["today"], house=house,
|
||||
)
|
||||
|
||||
|
||||
def _natal(chart: dict):
|
||||
natal_chart, asc_idx, _, _ = engine._compute_chart_from_args(_args(chart, 7))
|
||||
ascendant = natal_chart["ascendant"]
|
||||
asc_deg = ascendant.get("lon", ascendant.get("degree", 0))
|
||||
return natal_chart["planets"], asc_idx, asc_deg
|
||||
|
||||
|
||||
def _d9_targets(chart: dict, house: int):
|
||||
natal, asc_idx, asc_deg = _natal(chart)
|
||||
event_lord = engine.SIGN_LORDS[engine.SIGNS[(asc_idx + house - 1) % 12]]
|
||||
ll_name = engine.SIGN_LORDS[engine.SIGNS[asc_idx]]
|
||||
return engine._double_transit_d9_targets(asc_deg, natal, house, event_lord, ll_name), natal, asc_deg
|
||||
|
||||
|
||||
def _d1_cl_layers() -> dict:
|
||||
out = {}
|
||||
for chart in load_public_charts(ROOT):
|
||||
for house in HOUSES:
|
||||
result = engine.cmd_double_transit_pac(_args(chart, house))
|
||||
stats = result["stats"]
|
||||
out[f"{chart['id']}|{house}"] = {
|
||||
"d1": result["d1"],
|
||||
"cl": result["cl"],
|
||||
"d1_cl_double_transit": [row for row in result["double_transit"] if row["layer"] in ("D1", "CL")],
|
||||
"stats": {key: value for key, value in stats.items() if key.startswith(("d1_", "cl_")) or key == "chandra_lagna"},
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def charts() -> list[dict]:
|
||||
return load_public_charts(ROOT)
|
||||
|
||||
|
||||
def test_quick_gate_runs_this_file() -> None:
|
||||
import run_quality_gate
|
||||
|
||||
assert "tests/test_double_transit_d9_targets.py" in run_quality_gate.CORE_PYTEST_TARGETS
|
||||
|
||||
|
||||
def test_d9_house_target_is_the_d9_house_sign_not_the_d9_lagna(charts) -> None:
|
||||
for chart in charts:
|
||||
for house in range(2, 13):
|
||||
(d9_asc_idx, targets), _, _ = _d9_targets(chart, house)
|
||||
house_si = (d9_asc_idx + house - 1) % 12
|
||||
label = f"D9_{house}宫({engine.SIGNS[house_si]})"
|
||||
assert targets[label] == house_si * 30 + 15, (chart["id"], house)
|
||||
assert targets[label] != d9_asc_idx * 30 + 15, (chart["id"], house)
|
||||
|
||||
|
||||
def test_d9_house_1_target_is_still_the_d9_lagna(charts) -> None:
|
||||
for chart in charts:
|
||||
(d9_asc_idx, targets), _, asc_deg = _d9_targets(chart, 1)
|
||||
assert d9_asc_idx == engine._navamsa_idx(asc_deg)
|
||||
assert targets[f"D9_1宫({engine.SIGNS[d9_asc_idx]})"] == d9_asc_idx * 30 + 15
|
||||
|
||||
|
||||
def test_d9_lord_targets_are_navamsa_signs(charts) -> None:
|
||||
for chart in charts:
|
||||
for house in HOUSES:
|
||||
(d9_asc_idx, targets), natal, _ = _d9_targets(chart, house)
|
||||
d9_lord = engine.SIGN_LORDS[engine.SIGNS[(d9_asc_idx + house - 1) % 12]]
|
||||
lord_si = engine._navamsa_idx(natal[d9_lord]["degree"])
|
||||
# Before BUG-1060 this target was the lord's D1 degree.
|
||||
assert targets[f"D9_{d9_lord}(宫主)"] == lord_si * 30 + 15, (chart["id"], house)
|
||||
|
||||
|
||||
def test_command_checks_the_helper_targets_in_the_d9_frame(charts, monkeypatch) -> None:
|
||||
calls = []
|
||||
original = engine._check_pac
|
||||
|
||||
def recording(planet_name, planet_lon, target_lon, asc_idx):
|
||||
calls.append((planet_name, target_lon, asc_idx))
|
||||
return original(planet_name, planet_lon, target_lon, asc_idx)
|
||||
|
||||
monkeypatch.setattr(engine, "_check_pac", recording)
|
||||
for chart in charts:
|
||||
for house in (1, 7, 10):
|
||||
(d9_asc_idx, targets), _, _ = _d9_targets(chart, house)
|
||||
calls.clear()
|
||||
engine.cmd_double_transit_pac(_args(chart, house))
|
||||
for planet in ("Jupiter", "Saturn"):
|
||||
seen = [lon for name, lon, idx in calls if name == planet]
|
||||
# d1 (4 targets) → d9 → cl (2), in insertion order, per transit planet; for house 1
|
||||
# the D1 house lord is the lagna lord, so the D9 layer has 3 distinct targets.
|
||||
assert seen[4:4 + len(targets)] == list(targets.values()), (chart["id"], house, planet)
|
||||
assert {idx for name, _, idx in calls if name == planet} >= {d9_asc_idx}
|
||||
|
||||
|
||||
def test_public_chart_seventh_house_d9_row(charts) -> None:
|
||||
"""The BUG-1060 reproduction: Saturn's 10th aspect was on the D9 lagna, not on D9 house 7."""
|
||||
chart = next(item for item in charts if item["id"] == "steve_jobs")
|
||||
result = engine.cmd_double_transit_pac(_args(chart, 7))
|
||||
assert result["stats"]["d9_ascendant"] == "Capricorn"
|
||||
label = "D9_7宫(Cancer)"
|
||||
assert label not in result["d9"]["saturn"]
|
||||
assert result["d9"]["jupiter"][label] == [{"type": "Position", "desc": "同宫(7宫)"}]
|
||||
assert "D9_Moon(宫主)" not in result["d9"]["saturn"]
|
||||
|
||||
|
||||
def test_d1_and_chandra_lagna_layers_match_the_pre_fix_golden() -> None:
|
||||
golden = json.loads(GOLDEN.read_text(encoding="utf-8"))
|
||||
assert golden["provenance"]["charts"] == [chart["id"] for chart in load_public_charts(ROOT)]
|
||||
assert json.dumps(_d1_cl_layers(), ensure_ascii=False, sort_keys=True) == json.dumps(
|
||||
golden["layers"], ensure_ascii=False, sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def _capture() -> None:
|
||||
import os
|
||||
|
||||
if os.environ.get("PYTHONHASHSEED") != "0":
|
||||
raise SystemExit("Set PYTHONHASHSEED=0 before starting this process (ERR-111).")
|
||||
charts = load_public_charts(ROOT)
|
||||
payload = {
|
||||
"provenance": {
|
||||
"generator": "tests/test_double_transit_d9_targets.py --capture (jyotish_engine.cmd_double_transit_pac)",
|
||||
"charts": [chart["id"] for chart in charts],
|
||||
"reference_date": charts[0]["body"]["today"],
|
||||
"ayanamsa": charts[0]["body"]["ayanamsa"],
|
||||
"node_mode": charts[0]["body"]["node_mode"],
|
||||
"houses": list(HOUSES),
|
||||
"note": "D1 / Chandra Lagna layers only; captured before the BUG-1060 D9 fix and unchanged by it.",
|
||||
},
|
||||
"layers": _d1_cl_layers(),
|
||||
}
|
||||
GOLDEN.parent.mkdir(parents=True, exist_ok=True)
|
||||
GOLDEN.write_text(json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=1) + "\n", encoding="utf-8")
|
||||
print(f"wrote {GOLDEN.relative_to(ROOT)}")
|
||||
|
||||
|
||||
if __name__ == "__main__" and "--capture" in sys.argv:
|
||||
_capture()
|
||||
Reference in New Issue
Block a user