feat(consult): card carries aspects, co-tenants, combustion; yogas by domain
TASK-consult-card-affliction-data-20261001 T1-T4 (BUG-1154~1157, BUG-1159 mitigation). - Engine layer graha_drishti (aspects.calc_house_aspects; combustion via the full reading's own rule, extracted as jyotish_engine.is_combust, orbs unchanged); yoga_domains (fixed Raman packet table by rule_id, with the sieve's hit); slow_transits.saturn_from_moon_house. - Card: planets get aspected_by / conjunct_with / combust (true only); domain house rows get aspected_by; parents roles get lord_rows and significator_rows with functional class, D12 house lord and its sign; health houses [1,6,8,12], education [4,5,9]; annual copies saturn_from_moon_house; yogas filtered by domain tag with hits. - Budget (D6): antardashas as [lord, start, end], D9 vargottama only when true; all 12,000-char assertions unchanged. - Golden regenerated by the real engine; jyotish_api_server.py untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9b937c4adf
commit
19802a4874
@@ -20,6 +20,12 @@ existing reader consumes. It is kept out of `chart.modules` on purpose: the
|
||||
thematic report counts the modules it was handed (`full_reading_module_count`)
|
||||
and that existing value must not move.
|
||||
|
||||
TASK-consult-card-affliction-data-20261001 adds three outputs: the whole-sign
|
||||
graha drishti and combustion (`graha_drishti`, from `aspects.calc_house_aspects`
|
||||
and the full reading's `jyotish_engine.is_combust`), the domain tag of each
|
||||
sieved yoga (`yoga_domains`, a fixed packet table) and transit Saturn's house
|
||||
from the natal Moon (`slow_transits.saturn_from_moon_house`).
|
||||
|
||||
The annual pack is attached only when the declared route is `annual`. When the
|
||||
pack cannot be produced reliably it is reported as blocked with the reason;
|
||||
natal data is never substituted for the annual chart.
|
||||
@@ -45,6 +51,26 @@ REVERSAL_RULE = (
|
||||
"D1 in EXALTED / MOOLATRIKONA / OWN_SIGN and D9 in DEBILITATED / NEECHA_BHANGA, or the reverse; "
|
||||
"levels from jyotish_engine._get_dignity_level"
|
||||
)
|
||||
# Graha drishti casters (TASK-consult-card-affliction-data-20261001 D2): the
|
||||
# seven grahas; Rahu / Ketu receive aspects but cast none here (schools
|
||||
# differ), their influence reaches the card through house co-tenancy.
|
||||
DRISHTI_CASTERS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
|
||||
DRISHTI_RULE = (
|
||||
"Parashari whole-sign graha drishti (aspects.calc_house_aspects): every graha the 7th; "
|
||||
"Mars also 4 / 8, Jupiter 5 / 9, Saturn 3 / 10; Rahu / Ketu cast none"
|
||||
)
|
||||
# Domain tag of each Raman support packet (scripts/raman_support_observations.py).
|
||||
# A fixed table: a yoga's domain is the packet that governs it, never a guess
|
||||
# from its name (BUG-1157). The Nabhasa / Chandra batch describes the whole
|
||||
# chart, so it is `general`.
|
||||
YOGA_PACKET_DOMAINS = {
|
||||
"raman_bvr_nabhasa_chandra_sunapha_batch_closure_packet_2026_08_20.json": "general",
|
||||
"raman_bvr_wealth_core_batch_closure_packet_2026_08_20.json": "wealth",
|
||||
"raman_bvr_relationship_spouse_batch_closure_packet_2026_08_20.json": "marriage",
|
||||
"raman_bvr_relationship_children_batch_closure_packet_2026_08_20.json": "children",
|
||||
"raman_bvr_relationship_mother_batch_closure_packet_2026_08_20.json": "parents",
|
||||
"raman_bvr_relationship_sibling_core_batch_closure_packet_2026_08_20.json": "siblings",
|
||||
}
|
||||
TAJIKA_BOUNDARY = (
|
||||
"parameter_sensitive: Tajika / Varshaphala is verified on the first target set only, "
|
||||
"not a global closure; annual structure, not an event verdict"
|
||||
@@ -302,12 +328,18 @@ def build_slow_transits(engine, chart: dict, modules: dict, *, reference: dateti
|
||||
if isinstance(house, int) and house not in houses:
|
||||
houses.append(house)
|
||||
houses_in_window[body] = houses
|
||||
natal_moon_sign = _record(_record(chart.get("planets")).get("Moon")).get("sign")
|
||||
return {
|
||||
"status": "executed",
|
||||
"source": "jyotish_engine._calc_sidereal_planets_for_jd + ephemeris_events.build_ephemeris_events",
|
||||
"reference_date": reference.strftime("%Y-%m-%d"),
|
||||
"window": {"start": start.strftime("%Y-%m-%d"), "end": end.strftime("%Y-%m-%d")},
|
||||
"current": current,
|
||||
# BUG-1156: the house of transit Saturn's sign counted from the natal
|
||||
# Moon's sign (1 = Saturn over the natal Moon), whole-sign.
|
||||
"saturn_from_moon_house": _whole_sign_house(
|
||||
_record(current.get("Saturn")).get("sign"), _sign_index(natal_moon_sign),
|
||||
),
|
||||
"key_dates": events,
|
||||
"houses_in_window": houses_in_window,
|
||||
"boundary": scan.get("boundary") or "sign ingress and station scan, not a natal trigger",
|
||||
@@ -567,6 +599,103 @@ def build_inter_chart_linkage(engine, chart: dict, modules: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
# ── Graha drishti and combustion ────────────────────────────────────────────
|
||||
|
||||
def build_graha_drishti(engine, chart: dict) -> dict:
|
||||
"""Who aspects whom (whole-sign) and which planets are combust.
|
||||
|
||||
Aspected houses: `aspects.calc_house_aspects` for the seven grahas (the
|
||||
full reading's own Step 5 producer). `aspected_by` of a house is every
|
||||
caster whose aspected houses include it; `aspected_by` of a planet is that
|
||||
of the house it occupies. Combustion: `jyotish_engine.is_combust`, the
|
||||
full reading's rule and orbs; written only for the seven grahas.
|
||||
"""
|
||||
try:
|
||||
from aspects import calc_house_aspects
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
from scripts.aspects import calc_house_aspects
|
||||
planets = _record(chart.get("planets"))
|
||||
asc_lon = _planet_lon(chart.get("ascendant"))
|
||||
lagna_idx = _lagna_idx(chart)
|
||||
if asc_lon is None or lagna_idx is None or not planets:
|
||||
return _blocked("natal_longitudes_missing")
|
||||
rows: dict[str, dict] = {}
|
||||
houses: dict[str, dict] = {str(number): {"aspected_by": []} for number in range(1, 13)}
|
||||
for name in DRISHTI_CASTERS:
|
||||
lon = _planet_lon(planets.get(name))
|
||||
if lon is None:
|
||||
continue
|
||||
result = calc_house_aspects(lon, name, asc_lon)
|
||||
targets = sorted(int(house) for house in _record(result.get("aspects_to")))
|
||||
rows[name] = {"house": result.get("house"), "aspects_houses": targets}
|
||||
for house in targets:
|
||||
houses[str(house)]["aspected_by"].append(name)
|
||||
for name, pdata in planets.items():
|
||||
if not isinstance(pdata, dict):
|
||||
continue
|
||||
house = _record(rows.get(name)).get("house") or _whole_sign_house(pdata.get("sign"), lagna_idx)
|
||||
if not isinstance(house, int):
|
||||
continue
|
||||
row = rows.setdefault(name, {"house": house})
|
||||
row["aspected_by"] = list(houses[str(house)]["aspected_by"])
|
||||
sun_lon = _planet_lon(planets.get("Sun"))
|
||||
if sun_lon is not None:
|
||||
for name in DRISHTI_CASTERS:
|
||||
lon = _planet_lon(planets.get(name))
|
||||
if lon is not None and name in rows:
|
||||
rows[name]["combust"] = bool(engine.is_combust(name, lon, sun_lon))
|
||||
for row in rows.values():
|
||||
row.pop("house", None)
|
||||
return {
|
||||
"status": "executed",
|
||||
"source": "aspects.calc_house_aspects + jyotish_engine.is_combust",
|
||||
"rule": DRISHTI_RULE,
|
||||
"planets": rows,
|
||||
"houses": houses,
|
||||
}
|
||||
|
||||
|
||||
def _yoga_packet_tags() -> dict[str, str]:
|
||||
try:
|
||||
from raman_support_observations import packet_members
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
from scripts.raman_support_observations import packet_members
|
||||
tags: dict[str, str] = {}
|
||||
for filename, domain in YOGA_PACKET_DOMAINS.items():
|
||||
for member in packet_members(filename):
|
||||
if member.get("rule_id"):
|
||||
tags.setdefault(member["rule_id"], domain)
|
||||
return tags
|
||||
|
||||
|
||||
def build_yoga_domains(modules: dict) -> dict:
|
||||
"""The domain tag of each yoga in `modules.yogas` (BUG-1157).
|
||||
|
||||
The tag is the Raman packet that governs the yoga's rule id
|
||||
(`YOGA_PACKET_DOMAINS`); `hit` is the sieve's own value. A yoga whose rule
|
||||
id is in no packet carries no tag.
|
||||
"""
|
||||
entries = _record(modules.get("yogas")).get("yogas")
|
||||
if not isinstance(entries, list):
|
||||
return _blocked("yogas_missing")
|
||||
tags = _yoga_packet_tags()
|
||||
rows: list[dict] = []
|
||||
for entry in entries:
|
||||
row = _record(entry)
|
||||
if not isinstance(row.get("name"), str):
|
||||
continue
|
||||
out: dict[str, Any] = {"name": row["name"], "hit": bool(row.get("hit"))}
|
||||
tag = tags.get(str(row.get("rule_id") or ""))
|
||||
if tag:
|
||||
out["domain"] = tag
|
||||
rows.append(out)
|
||||
return {
|
||||
"status": "executed",
|
||||
"source": "modules.yogas (Raman sieve) + YOGA_PACKET_DOMAINS",
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
|
||||
# ── Annual Tajika / Varshaphala ─────────────────────────────────────────────
|
||||
|
||||
def _annual_payload(birth_payload: dict, target_year: int) -> dict:
|
||||
@@ -758,6 +887,8 @@ def attach_native_consultation_layers(chart: dict, *, birth_payload: dict, body:
|
||||
layers["argala"] = _guard(build_argala, chart)
|
||||
layers["dispositor_chains"] = _guard(build_dispositor_chains, engine, chart)
|
||||
layers["inter_chart_linkage"] = _guard(build_inter_chart_linkage, engine, chart, modules)
|
||||
layers["graha_drishti"] = _guard(build_graha_drishti, engine, chart)
|
||||
layers["yoga_domains"] = _guard(build_yoga_domains, modules)
|
||||
if "annual" in routes:
|
||||
layers["annual_tajika"] = _guard(build_annual_tajika, birth_payload, chart, reference=reference)
|
||||
else:
|
||||
|
||||
@@ -10703,6 +10703,19 @@ def _oracle_progress_snapshot():
|
||||
}
|
||||
|
||||
|
||||
# Combustion orbs of the full reading (Step 5 shadbala block). The Moon, Rahu
|
||||
# and Ketu have no orb here; the Sun is never combust. Shared with the web
|
||||
# consultation layer (consultation_native_layers.build_graha_drishti) so both
|
||||
# read the same rule (BUG-1154).
|
||||
COMBUSTION_ORBS = {'Mars': 17, 'Mercury': 12, 'Jupiter': 11, 'Venus': 10, 'Saturn': 15}
|
||||
|
||||
|
||||
def is_combust(planet_name, longitude, sun_longitude):
|
||||
"""True when `planet_name` is within its combustion orb of the Sun."""
|
||||
separation = abs(((float(longitude) - float(sun_longitude) + 180) % 360) - 180)
|
||||
return planet_name != 'Sun' and separation <= COMBUSTION_ORBS.get(planet_name, 0)
|
||||
|
||||
|
||||
def _functional_benefic_malefic_snapshot(planets, ascendant):
|
||||
try:
|
||||
from functional_benefics import derive_functional_benefic_malefic
|
||||
@@ -16327,7 +16340,6 @@ def cmd_full_reading(args):
|
||||
shadbala_result['weak_planets'] = len(sb_planets) - strong_count
|
||||
shadbala_result['status'] = '优秀' if strong_count >= 5 else '良好' if strong_count >= 3 else '一般'
|
||||
sun_longitude = float(planet_lons.get('Sun', 0.0))
|
||||
combustion_orbs = {'Mars': 17, 'Mercury': 12, 'Jupiter': 11, 'Venus': 10, 'Saturn': 15}
|
||||
for planet_name, planet in planets.items():
|
||||
if not isinstance(planet, dict) or planet_name in {'Rahu', 'Ketu'}:
|
||||
continue
|
||||
@@ -16335,8 +16347,7 @@ def cmd_full_reading(args):
|
||||
if longitude is None:
|
||||
continue
|
||||
planet.update(get_kp_lords(float(longitude)))
|
||||
separation = abs(((float(longitude) - sun_longitude + 180) % 360) - 180)
|
||||
planet['combust'] = planet_name != 'Sun' and separation <= combustion_orbs.get(planet_name, 0)
|
||||
planet['combust'] = is_combust(planet_name, float(longitude), sun_longitude)
|
||||
planet['r_c'] = 'R' if planet.get('retrograde') else 'C' if planet['combust'] else ''
|
||||
strength = sb_planets.get(planet_name) if isinstance(sb_planets.get(planet_name), dict) else {}
|
||||
if strength:
|
||||
|
||||
Reference in New Issue
Block a user