Files
Jyotisha/scripts/rectification/house_table.py
Jesse_Chen a3196584d2 feat(rectification): add refinement packet without unique-minute claims
Surface D9/D10 change minutes, event-dasha match copy, dual-dasha conflict, and a post-adopt consult handoff so users can keep narrowing or start a reading from a representative time.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 07:47:10 +08:00

125 lines
3.6 KiB
Python

"""Compact D1 house table for the public rectification surface.
Sign names and occupants only. Longitudes, coordinates, scores and
fingerprints stay out of this projection.
"""
from __future__ import annotations
from typing import Any
from scripts.jyotish_engine import SIGNS, SIGNS_CN
PLANET_ORDER = (
"Sun",
"Moon",
"Mars",
"Mercury",
"Jupiter",
"Venus",
"Saturn",
"Rahu",
"Ketu",
)
PLANET_ZH = {
"Sun": "太阳",
"Moon": "月亮",
"Mars": "火星",
"Mercury": "水星",
"Jupiter": "木星",
"Venus": "金星",
"Saturn": "土星",
"Rahu": "罗睺",
"Ketu": "计都",
}
SIGN_LORDS = {
"Aries": "Mars",
"Taurus": "Venus",
"Gemini": "Mercury",
"Cancer": "Moon",
"Leo": "Sun",
"Virgo": "Mercury",
"Libra": "Venus",
"Scorpio": "Mars",
"Sagittarius": "Jupiter",
"Capricorn": "Saturn",
"Aquarius": "Saturn",
"Pisces": "Jupiter",
}
def _sign_cn(sign: str | None) -> str | None:
if not isinstance(sign, str) or sign not in SIGNS_CN:
return None
return SIGNS_CN[sign]
def _house_number(value: Any, sign: str | None, asc_idx: int | None) -> int | None:
if isinstance(value, int) and 1 <= value <= 12:
return value
if not sign or sign not in SIGNS or asc_idx is None:
return None
return ((SIGNS.index(sign) - asc_idx) % 12) + 1
def compact_house_table(chart: Any, *, time: str) -> dict[str, Any] | None:
if not isinstance(chart, dict) or not isinstance(time, str):
return None
ascendant = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {}
lagna = _sign_cn(str(ascendant.get("sign") or "") or None)
if not lagna:
return None
try:
asc_idx = SIGNS.index(str(ascendant["sign"]))
except (KeyError, ValueError):
return None
occupants: dict[int, list[str]] = {house: [] for house in range(1, 13)}
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
for name in PLANET_ORDER:
data = planets.get(name)
if not isinstance(data, dict):
continue
house = _house_number(data.get("house"), str(data.get("sign") or "") or None, asc_idx)
label = PLANET_ZH.get(name)
if house is None or not label or label in occupants[house]:
continue
occupants[house].append(label)
houses = []
for house in range(1, 13):
sign = _sign_cn(SIGNS[(asc_idx + house - 1) % 12])
if not sign:
return None
houses.append({
"house": house,
"sign": sign,
"occupants": occupants[house],
})
return {
"time": time[:5],
"lagna": lagna,
"houses": houses,
}
def compact_house_table_from_contexts(contexts: Any, time: str | None) -> dict[str, Any] | None:
if not isinstance(time, str) or not time or not isinstance(contexts, list):
return None
wanted = time[:5]
for context in contexts:
if not isinstance(context, dict):
continue
feature = context.get("feature") if isinstance(context.get("feature"), dict) else {}
feature_time = feature.get("time")
candidate_at = context.get("candidate_at")
context_time = feature_time if isinstance(feature_time, str) else None
if context_time is None and hasattr(candidate_at, "strftime"):
context_time = candidate_at.strftime("%H:%M")
if context_time != wanted:
continue
return compact_house_table(context.get("chart"), time=wanted)
return None