Every D9 target name contains the "9" of "D9", so the digit comparison never paired D1 N宫 with D9_N宫 and, for house 9, paired D1 9宫 with any D9 planet target. Targets now carry structured metadata (layer/kind/house/planet); the cross-layer loop pairs only the same house or targets that both point at the D1 house-N lord. Target names and D1/D9/CL layer results are unchanged (D1/CL golden regenerated byte-identical); 48 samples: cross entries 31 -> 38. Evidence-card golden regenerated with the capture script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
206 lines
9.6 KiB
Python
206 lines
9.6 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; `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)
|
|
assert result["summary"].startswith("⚠️ 跨层间接")
|
|
|
|
|
|
def test_ninth_house_no_longer_pairs_with_an_unrelated_d9_target(charts) -> None:
|
|
"""Public chart, house 9: Mars_D9 is the lagna lord's D9 sign, not the 9th house."""
|
|
result, ctx = _public(charts, "elizabeth_taylor", 9)
|
|
assert ctx["ll_name"] == "Mars" and ctx["event_lord"] == "Moon"
|
|
# Both hits are still there; only the pairing changed.
|
|
assert "9宫(Cancer)" in result["d1"]["jupiter"]
|
|
assert "Mars_D9(Capricorn)" in result["d9"]["saturn"]
|
|
assert all(d1_target != "9宫(Cancer)" for _, d1_target, _, _ in _cross(result))
|
|
assert all(not d9_target.startswith("Mars_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.
|
|
assert same_house == {("elizabeth_taylor", 6), ("elizabeth_taylor", 12), ("steve_jobs", 2), ("steve_jobs", 6), ("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)
|