Files
Jyotisha/scripts/marriage_match_service/full_dossier.py
T
jesse-uxGrok (agent loop)Cursor
123295135a
Independent Staging Quality Gate / validate (push) Successful in 16m50s
Independent Staging Quality Gate / publish (push) Successful in 10m2s
fix(synastry): restore report paging and finish the third review gaps
The list query had been split into four reads, so older pages no longer followed one cursor. A7 was read from the wrong module, and the shell asked for the product switch on every mount.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 15:53:06 +08:00

226 lines
9.2 KiB
Python

"""Glue around the upstream full-synastry skill.
Two birth payloads become two native full-readings, then the untouched
``build_full_synastry_dossier``. Raman is applied to the snapshots the skill
already extracted. This module does not edit those upstream files.
"""
from __future__ import annotations
import importlib.util
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Callable
from marriage_match import build_marriage_match_dossier
ROOT = Path(__file__).resolve().parents[2]
DOSSIER_SCRIPT = (
ROOT
/ "skills"
/ "jyotish-full-synastry-reading"
/ "scripts"
/ "build_full_synastry_dossier.py"
)
RULES_VERSION = "bde5d615:marriage_match_product_v1"
RAMAN_PROFILE = "raman_muhurtha_v1"
ReadingFn = Callable[[dict[str, Any]], dict[str, Any]]
def _dossier_builder():
spec = importlib.util.spec_from_file_location(
"build_full_synastry_dossier",
DOSSIER_SCRIPT,
)
if spec is None or spec.loader is None:
raise RuntimeError("full synastry dossier script is missing")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.build_full_synastry_dossier
def _parse_day(value: Any) -> datetime | None:
if not isinstance(value, str) or not value.strip():
return None
text = value.strip()[:10]
try:
return datetime.strptime(text, "%Y-%m-%d")
except ValueError:
return None
def _clip_period(period: dict[str, Any], level: str, window_start: datetime, window_end: datetime) -> dict[str, Any] | None:
start = _parse_day(period.get("start") or period.get("start_datetime"))
end = _parse_day(period.get("end") or period.get("end_datetime"))
if start is None or end is None or end <= window_start or start >= window_end:
return None
lord = period.get("lord")
if not isinstance(lord, str) or not lord:
return None
lord_cn = period.get("lord_cn") if isinstance(period.get("lord_cn"), str) else lord
return {
"level": level,
"lord": lord,
"lordCn": lord_cn,
"start": max(start, window_start).strftime("%Y-%m-%d"),
"end": min(end, window_end).strftime("%Y-%m-%d"),
}
def upcoming_vimshottari(reading: dict[str, Any], today: str, years: int = 5) -> list[dict[str, Any]]:
"""Mahadasha and antardasha slices that overlap the next few years."""
packet = reading.get("raw_full_reading") if isinstance(reading.get("raw_full_reading"), dict) else reading
modules = packet.get("modules") if isinstance(packet, dict) else {}
dasha = modules.get("dasha") if isinstance(modules, dict) else {}
timeline = dasha.get("timeline") if isinstance(dasha, dict) else []
start = _parse_day(today) or datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
end = start + timedelta(days=365 * years)
rows: list[dict[str, Any]] = []
if not isinstance(timeline, list):
return rows
for mahadasha in timeline:
if not isinstance(mahadasha, dict):
continue
clipped = _clip_period(mahadasha, "md", start, end)
if clipped:
rows.append(clipped)
for antardasha in mahadasha.get("antardasha_timeline") or []:
if not isinstance(antardasha, dict):
continue
child = _clip_period(antardasha, "ad", start, end)
if child:
rows.append(child)
return rows
_SIGNS = (
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
)
_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 _reading_packet(reading: dict[str, Any]) -> dict[str, Any]:
nested = reading.get("raw_full_reading")
return nested if isinstance(nested, dict) else reading
def _sign_name(value: Any) -> str | None:
if isinstance(value, str) and value.strip():
return value.strip()
if not isinstance(value, dict):
return None
for key in ("sign", "sign_name", "rasi"):
sign = value.get(key)
if isinstance(sign, str) and sign.strip():
return sign.strip()
for key in ("longitude", "degree_raw", "lon", "degree"):
longitude = value.get(key)
if isinstance(longitude, (int, float)) and not isinstance(longitude, bool):
return _SIGNS[int(float(longitude) % 360.0) // 30]
return None
def _named_planet(root: Any, path: tuple[str, ...]) -> str | None:
value = root
for key in path:
value = value.get(key) if isinstance(value, dict) else None
if isinstance(value, str) and value.strip():
return value.strip()
if isinstance(value, dict):
planet = value.get("planet") or value.get("name")
if isinstance(planet, str) and planet.strip():
return planet.strip()
return None
def relationship_structure(reading: dict[str, Any]) -> dict[str, Any]:
"""Signs and relationship points the longitude-only snapshot does not carry."""
modules = _reading_packet(reading).get("modules")
modules = modules if isinstance(modules, dict) else {}
chart = modules.get("chart") if isinstance(modules.get("chart"), dict) else {}
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
ascendant = _sign_name(chart.get("ascendant"))
seventh = _SIGN_LORDS[_SIGNS[(_SIGNS.index(ascendant) + 6) % 12]] if ascendant in _SIGN_LORDS else None
jaimini = modules.get("jaimini") if isinstance(modules.get("jaimini"), dict) else {}
darakaraka = (
_named_planet(jaimini, ("chara_karakas", "DK"))
or _named_planet(jaimini, ("chara_karakas", "Darakaraka"))
or _named_planet(jaimini, ("chara_karaka_7", "karaka_table", "Darakaraka"))
or _named_planet(jaimini, ("chara_karaka_8", "karaka_table", "Darakaraka"))
)
special = modules.get("special_lagnas") if isinstance(modules.get("special_lagnas"), dict) else {}
if not special and isinstance(jaimini.get("special_lagnas"), dict):
special = jaimini.get("special_lagnas")
arudha = jaimini.get("arudha_padas") if isinstance(jaimini.get("arudha_padas"), dict) else {}
if not arudha and isinstance(modules.get("arudha_padas"), dict):
arudha = modules.get("arudha_padas")
padas = arudha.get("padas") if isinstance(arudha.get("padas"), dict) else arudha
upapada = _sign_name(special.get("Upapada_Lagna") or special.get("UL") or arudha.get("upapada"))
if upapada is None and isinstance(padas, dict):
upapada = _sign_name(padas.get("UL"))
a7 = _sign_name(padas.get("A7") if isinstance(padas, dict) else None)
if a7 is None:
a7 = _sign_name(special.get("A7") or special.get("Darapada") or arudha.get("A7"))
vargas = modules.get("varga_full") if isinstance(modules.get("varga_full"), dict) else {}
navamsa = vargas.get("D9_Navamsa") if isinstance(vargas.get("D9_Navamsa"), dict) else {}
d9 = _sign_name(navamsa.get("Ascendant") or navamsa.get("ascendant"))
return {
"ascendant": {"sign": ascendant} if ascendant else {},
"planets": {
"Venus": {"sign": _sign_name(planets.get("Venus"))} if _sign_name(planets.get("Venus")) else {},
"Moon": {"sign": _sign_name(planets.get("Moon"))} if _sign_name(planets.get("Moon")) else {},
},
"seventh_lord": seventh,
"darakaraka": darakaraka,
"upapada": upapada,
"A7": a7,
"d9": {"ascendant": d9} if d9 else {},
}
def build_synastry_full_dossier(
handler: Any,
person_a: dict[str, Any],
person_b: dict[str, Any],
*,
read_chart: ReadingFn | None = None,
) -> dict[str, Any]:
"""Run two full-readings in parallel, then both marriage-match profiles."""
read = read_chart or handler._compute_full_reading_for_thematic
with ThreadPoolExecutor(max_workers=2) as pool:
future_a = pool.submit(read, person_a)
future_b = pool.submit(read, person_b)
reading_a = future_a.result()
reading_b = future_b.result()
dossier = _dossier_builder()(reading_a, reading_b)
if isinstance(dossier, dict):
dossier["local_structures"] = {
"person_a": relationship_structure(reading_a if isinstance(reading_a, dict) else {}),
"person_b": relationship_structure(reading_b if isinstance(reading_b, dict) else {}),
}
today = str(person_a.get("today") or person_b.get("today") or "")
snapshots = dossier.get("chart_snapshots") if isinstance(dossier, dict) else {}
snapshots = snapshots if isinstance(snapshots, dict) else {}
raman = build_marriage_match_dossier(
snapshots.get("person_a") or {},
snapshots.get("person_b") or {},
profile_id=RAMAN_PROFILE,
)
return {
"rules_version": RULES_VERSION,
"dossier": dossier,
"raman_marriage_match": raman,
"timing_periods": {
"person_a": upcoming_vimshottari(reading_a if isinstance(reading_a, dict) else {}, today),
"person_b": upcoming_vimshottari(reading_b if isinstance(reading_b, dict) else {}, today),
},
}