Files
Jyotisha/tests/test_double_transit_cross_layer_pairing.py
T
Jesse_ChenandClaude Opus 5.5 db5a0509da fix(transit): double-transit rows in a stable order; cross-layer tests follow the aspect fix (BUG-1185)
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
2026-10-02 14:29:28 +08:00

223 lines
11 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.
"""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)