With the aspect count from 1 (42d1582f) the public charts get same-layer
overlaps for the first time, and the overlap / cross-layer loops iterated
Python sets, so the double_transit list order changed with PYTHONHASHSEED
(the D1/CL golden failed under seeds 1 and 2). The loops now iterate sorted
names; the golden is recaptured and passes under seeds 0/1/2/5.
tests/test_double_transit_cross_layer_pairing.py: three expectations updated
with original / new / reason (Jobs house-7 summary now leads with the D1
layer; the ninth-house non-pairing example moves to Jobs house 9; the
same-house set is recomputed).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
223 lines
11 KiB
Python
223 lines
11 KiB
Python
"""BUG-1061: Double Transit PAC cross-layer (D1+D9) entries pair targets by identity, not name digits.
|
||
|
||
Before the fix the pairing compared the digits in the target names; every D9
|
||
name contains the "9" of "D9", so D1 ``7宫`` never met ``D9_7宫`` ("7" vs "97")
|
||
while D1 ``9宫`` met any ``{planet}_D9`` / ``D9_{planet}(宫主)`` target.
|
||
|
||
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). D1 / D9 / CL layer results are unchanged
|
||
by this fix (they moved later with BUG-1185, the aspect count from 1); `tests/test_double_transit_d9_targets.py` holds the D1 / CL golden.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
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
|
||
|
||
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 _context(chart: dict, house: int) -> dict:
|
||
natal_chart, asc_idx, _, _ = engine._compute_chart_from_args(_args(chart, house))
|
||
ascendant = natal_chart["ascendant"]
|
||
asc_deg = ascendant.get("lon", ascendant.get("degree", 0))
|
||
event_sign = engine.SIGNS[(asc_idx + house - 1) % 12]
|
||
return {
|
||
"natal": natal_chart["planets"],
|
||
"asc_deg": asc_deg,
|
||
"event_sign": event_sign,
|
||
"event_lord": engine.SIGN_LORDS[event_sign],
|
||
"ll_name": engine.SIGN_LORDS[natal_chart["ascendant"]["sign"]],
|
||
"opposite_lord": engine.SIGN_LORDS[engine.SIGNS[(asc_idx + 6) % 12]],
|
||
}
|
||
|
||
|
||
def _meta(ctx: dict, house: int) -> dict:
|
||
return engine._double_transit_target_meta(
|
||
ctx["asc_deg"], ctx["natal"], house, ctx["event_sign"], ctx["event_lord"], ctx["ll_name"], ctx["opposite_lord"],
|
||
)
|
||
|
||
|
||
def _cross(result: dict) -> list[tuple[str, str, str, str]]:
|
||
"""(D1 planet, D1 target, D9 planet, D9 target) for every D1+D9 entry (parsed in the test only)."""
|
||
rows = []
|
||
for row in result["double_transit"]:
|
||
if row["layer"] != "D1+D9":
|
||
continue
|
||
left, right = row["target"].split(" + ")
|
||
d1_planet, d1_target = left.split("(D1)", 1)
|
||
d9_planet, d9_target = right.split("(D9)", 1)
|
||
rows.append((d1_planet, d1_target, d9_planet, d9_target))
|
||
return rows
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def charts() -> list[dict]:
|
||
return load_public_charts(ROOT)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def runs(charts) -> dict:
|
||
out = {}
|
||
for chart in charts:
|
||
for house in HOUSES:
|
||
ctx = _context(chart, house)
|
||
out[(chart["id"], house)] = {
|
||
"result": engine.cmd_double_transit_pac(_args(chart, house)),
|
||
"ctx": ctx,
|
||
"meta": _meta(ctx, house),
|
||
}
|
||
return out
|
||
|
||
|
||
def test_quick_gate_runs_this_file() -> None:
|
||
import run_quality_gate
|
||
|
||
assert "tests/test_double_transit_cross_layer_pairing.py" in run_quality_gate.CORE_PYTEST_TARGETS
|
||
|
||
|
||
def test_meta_names_are_the_command_target_names(runs) -> None:
|
||
for (chart_id, house), run in runs.items():
|
||
ctx, meta = run["ctx"], run["meta"]
|
||
d1_names = {
|
||
f"{house}宫({ctx['event_sign']})", f"{ctx['event_lord']}(宫主)",
|
||
f"{ctx['ll_name']}(LL)", f"{ctx['opposite_lord']}(对宫主)",
|
||
}
|
||
_, d9_targets = engine._double_transit_d9_targets(
|
||
ctx["asc_deg"], ctx["natal"], house, ctx["event_lord"], ctx["ll_name"],
|
||
)
|
||
assert set(meta["D1"]) == d1_names, (chart_id, house)
|
||
assert set(meta["D9"]) == set(d9_targets), (chart_id, house)
|
||
result = run["result"]
|
||
for planet in ("jupiter", "saturn"):
|
||
assert set(result["d1"][planet]) <= set(meta["D1"]), (chart_id, house)
|
||
assert set(result["d9"][planet]) <= set(meta["D9"]), (chart_id, house)
|
||
|
||
|
||
def test_identity_rules_on_structured_meta() -> None:
|
||
def meta(layer, kind, house, planet=None):
|
||
return [{"layer": layer, "kind": kind, "house": house, "planet": planet}]
|
||
|
||
ids = engine._double_transit_target_identities
|
||
# D1 9th house vs the lagna lord's D9 sign: the BUG-1061 false pair.
|
||
assert not ids(meta("D1", "house", 9), 9, "Moon") & ids(meta("D9", "lagna_lord_d9_sign", 1, "Mars"), 9, "Moon")
|
||
# D1 7th house vs D9 7th house: the pair the digit rule never made ("7" vs "97").
|
||
assert ids(meta("D1", "house", 7), 7, "Venus") & ids(meta("D9", "house", 7), 7, "Venus")
|
||
# D1 house-N lord vs that lord's D9 sign (KN Rao) and vs the D9 house-N lord when it is the same planet.
|
||
lord = ids(meta("D1", "house_lord", 7, "Venus"), 7, "Venus")
|
||
assert lord & ids(meta("D9", "lord_d9_sign", 7, "Venus"), 7, "Venus")
|
||
assert lord & ids(meta("D9", "d9_house_lord", 7, "Venus"), 7, "Venus")
|
||
# ... but not when another planet rules D9 house N.
|
||
assert not lord & ids(meta("D9", "d9_house_lord", 7, "Moon"), 7, "Venus")
|
||
# Lagna lord that is not the event lord never pairs (it would repeat in all 12 house calls).
|
||
assert not ids(meta("D1", "lagna_lord", 1, "Mars"), 7, "Venus") & ids(meta("D9", "lagna_lord_d9_sign", 1, "Mars"), 7, "Venus")
|
||
# A house target never pairs with a planet target.
|
||
assert not ids(meta("D1", "house", 7), 7, "Venus") & lord
|
||
|
||
|
||
def _public(charts, chart_id: str, house: int) -> tuple[dict, dict]:
|
||
chart = next(item for item in charts if item["id"] == chart_id)
|
||
return engine.cmd_double_transit_pac(_args(chart, house)), _context(chart, house)
|
||
|
||
|
||
def test_seventh_house_pairs_d1_house_with_d9_house(charts) -> None:
|
||
"""Public chart, house 7: Saturn on D1 7宫 and Jupiter on D9_7宫 are one cross-layer entry."""
|
||
result, _ = _public(charts, "steve_jobs", 7)
|
||
assert "7宫(Pisces)" in result["d1"]["saturn"]
|
||
assert "D9_7宫(Cancer)" in result["d9"]["jupiter"]
|
||
assert ("Saturn", "7宫(Pisces)", "Jupiter", "D9_7宫(Cancer)") in _cross(result)
|
||
# 原值: summary 以「⚠️ 跨层间接」开头(D1 层没有双重过运)
|
||
# 新值: 「⚠️ D1 层 Double Transit 激活…」
|
||
# 原因: BUG-1185(TASK-upstream-sync4-20261002 T2)相位改从 1 数后,木星 9 宫相位落到 D1 7 宫,
|
||
# D1 层本身已成双重过运,摘要按 D1 优先;跨层条目仍在(上一行)
|
||
assert result["summary"].startswith("⚠️ D1 层 Double Transit 激活")
|
||
|
||
|
||
def test_ninth_house_no_longer_pairs_with_an_unrelated_d9_target(charts) -> None:
|
||
"""Public chart, house 9: Mercury_D9 is the lagna lord's D9 sign, not the 9th house.
|
||
|
||
原值: 泰勒 9 宫(木星在 D1 9宫(Cancer)、土星在 Mars_D9(Capricorn))
|
||
新值: 乔布斯 9 宫(土星在 D1 9宫(Taurus)、木星在 Mercury_D9(Cancer))
|
||
原因: BUG-1185 相位改从 1 数后泰勒那组命中不再同时出现;换成同一结构(D1 9 宫 + 命主
|
||
D9 星座、命主不是事件宫主)的另一张公开盘,断言不变。
|
||
"""
|
||
result, ctx = _public(charts, "steve_jobs", 9)
|
||
assert ctx["ll_name"] == "Mercury" and ctx["event_lord"] == "Venus"
|
||
# Both hits are there; the pairing must not join them.
|
||
assert "9宫(Taurus)" in result["d1"]["saturn"]
|
||
assert "Mercury_D9(Cancer)" in result["d9"]["jupiter"]
|
||
assert all(d1_target != "9宫(Taurus)" for _, d1_target, _, _ in _cross(result))
|
||
assert all(not d9_target.startswith("Mercury_D9") for _, _, _, d9_target in _cross(result))
|
||
|
||
|
||
def test_every_cross_entry_joins_the_same_event_significator(runs) -> None:
|
||
for (chart_id, house), run in runs.items():
|
||
meta, event_lord = run["meta"], run["ctx"]["event_lord"]
|
||
for _, d1_target, _, d9_target in _cross(run["result"]):
|
||
d1_rows, d9_rows = meta["D1"][d1_target], meta["D9"][d9_target]
|
||
d1_house = any(row["kind"] == "house" for row in d1_rows)
|
||
d9_house = any(row["kind"] == "house" for row in d9_rows)
|
||
assert d1_house == d9_house, (chart_id, house, d1_target, d9_target)
|
||
if d1_house:
|
||
assert d1_rows[0]["house"] == d9_rows[0]["house"] == house
|
||
else:
|
||
assert {row["planet"] for row in d1_rows} == {event_lord}, (chart_id, house, d1_target)
|
||
assert event_lord in {row["planet"] for row in d9_rows}, (chart_id, house, d9_target)
|
||
|
||
|
||
def test_every_same_house_hit_becomes_a_cross_entry(runs) -> None:
|
||
same_house = set()
|
||
for (chart_id, house), run in runs.items():
|
||
result, meta = run["result"], run["meta"]
|
||
cross = _cross(result)
|
||
d1_house = next(name for name, rows in meta["D1"].items() if rows[0]["kind"] == "house")
|
||
d9_house = next(name for name, rows in meta["D9"].items() if rows[0]["kind"] == "house")
|
||
for d1_planet, d9_planet in (("jupiter", "saturn"), ("saturn", "jupiter")):
|
||
if d1_house in result["d1"][d1_planet] and d9_house in result["d9"][d9_planet]:
|
||
assert (d1_planet.title(), d1_house, d9_planet.title(), d9_house) in cross, (chart_id, house)
|
||
same_house.add((chart_id, house))
|
||
# On the library reference date these are the public-chart houses where both hits happen.
|
||
# 原值: {泰勒 6、12;乔布斯 2、6、7}
|
||
# 新值: 下列 10 格
|
||
# 原因: BUG-1185 相位改从 1 数(土 3/7/10、木 5/7/9 落在正确的宫),命中的宫整体平移
|
||
assert same_house == {
|
||
("barack_obama", 12), ("elizabeth_taylor", 3), ("elizabeth_taylor", 5), ("elizabeth_taylor", 7),
|
||
("elizabeth_taylor", 9), ("elizabeth_taylor", 11), ("steve_jobs", 1), ("steve_jobs", 3),
|
||
("steve_jobs", 5), ("steve_jobs", 7),
|
||
}
|
||
|
||
|
||
def test_event_lord_pairs_are_kept(runs) -> None:
|
||
"""The pre-fix rule (both targets name the D1 event lord) is expressed structurally and still pairs."""
|
||
for (chart_id, house), run in runs.items():
|
||
result, meta, event_lord = run["result"], run["meta"], run["ctx"]["event_lord"]
|
||
cross = _cross(result)
|
||
for d1_planet, d9_planet in (("jupiter", "saturn"), ("saturn", "jupiter")):
|
||
for d1_target in result["d1"][d1_planet]:
|
||
if meta["D1"][d1_target][0]["planet"] != event_lord:
|
||
continue
|
||
for d9_target in result["d9"][d9_planet]:
|
||
if event_lord in {row["planet"] for row in meta["D9"][d9_target]}:
|
||
assert (d1_planet.title(), d1_target, d9_planet.title(), d9_target) in cross, (chart_id, house)
|