Merge remote-tracking branch 'origin/staging' into codex/synastry-full-report-20261018

Renumber the synastry records to BUG-1394 and BUG-1395 so they no longer collide with BUG-1393 from issue 84.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-19 15:34:29 +08:00
co-authored by Grok (agent loop) Cursor
11 changed files with 502 additions and 109 deletions
+8 -6
View File
@@ -16124,18 +16124,20 @@ def cmd_full_reading(args):
planet_lons,
)
report['modules']['navamsha_dasha'] = _public_secondary_dasha_module(secondary_dashas.get('navamsha'))
report['modules']['niryaana_shoola_dasha'] = _public_secondary_dasha_module(
secondary_dashas.get('niryaana_shoola')
)
except Exception as e:
report['modules']['navamsha_dasha'] = {
'status': 'blocked',
'reason': f'navamsha_dasha_producer_failed:{e}',
'periods': [],
}
# F2b: do not publish the current Niryaana Shoola table. It matches Shoola.
# The synastry validator marks this row not applicable, so the pair task stays open.
report['modules']['niryaana_shoola_dasha'] = {
'execution_status': 'blocked',
'reason': '口径更正中,见 TASK-niryaana-shoola-pvr-20261018',
}
report['modules']['niryaana_shoola_dasha'] = {
'status': 'blocked',
'reason': f'niryaana_shoola_dasha_producer_failed:{e}',
'periods': [],
}
_record_stage_timing(
stage_timings,
'dasha_and_core_varga_stack',
+7
View File
@@ -140,6 +140,13 @@ def render_rashi_dasha_chapter(packet: Mapping) -> str:
section = sections[key]
title = title_en if language == "en" else title_zh
lines.extend([f"### {title}", "", _method_line(section, language), ""])
if key == "niryaana_shoola" and section.get("status") == "listed":
lines.extend([
"Child periods stay off this table: the cited book does not give a separate rule for them."
if language == "en"
else "子运规则未定,暂不列出。",
"",
])
if key == "chara" and section.get("status") == "listed":
lines.extend([
"This section is the K.N. Rao Chara table. It is not the Narayana table."
+177 -9
View File
@@ -38,6 +38,17 @@ except ImportError: # pragma: no cover - package import
_MOVABLE = {0, 3, 6, 9}
_FIXED = {1, 4, 7, 10}
_DUAL = {2, 5, 8, 11}
# Nine grahas only. Outer planets and upagrahas are ignored. Nodes are not weighted.
_AYUR_RASI_STRENGTH_BODIES = (
"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu",
)
_EXALTED_SIGNS = {
"Sun": {0}, "Moon": {1}, "Mars": {9}, "Mercury": {5},
"Jupiter": {3}, "Venus": {11}, "Saturn": {6},
}
_NIRYAANA_SOURCE_ZH = "PVR《Vedic Astrology: An Integrated Approach》"
_NIRYAANA_SOURCE_EN = "PVR, Vedic Astrology: An Integrated Approach"
_NIRYAANA_PROFILE = "pvr_niryaana_shoola_v1"
def _as_date(value: date | datetime | str) -> date:
@@ -303,6 +314,171 @@ def _drig_children(md_sign: int, start: date, end: date, graha: Mapping, planet_
return _equal_children(order, start, end)
def niryaana_shoola_years(sign_idx: int) -> int:
"""Movable, fixed, and dual signs last 7, 8, and 9 years."""
sign_idx = int(sign_idx) % 12
if sign_idx in _FIXED:
return 8
if sign_idx in _DUAL:
return 9
return 7
def select_niryaana_shoola_start_sign(
*,
ascendant_longitude: float,
planet_longitudes: Mapping[str, float],
) -> dict:
"""Pick the stronger of the second and eighth signs for a longevity dasha.
Compare occupant count, then every planet's Jaimini sign aspect (the sign
lord is not weighted), then whether an occupant is exalted. A three-way
tie stays off the table. Count only the nine grahas.
"""
lagna = sign_index(float(ascendant_longitude))
second, eighth = (lagna + 1) % 12, (lagna + 7) % 12
positions = {
name: float(longitude)
for name, longitude in planet_longitudes.items()
if name in _AYUR_RASI_STRENGTH_BODIES and isinstance(longitude, (int, float))
}
candidates = {}
for sign_idx in (second, eighth):
occupants = sorted(
name for name, longitude in positions.items()
if sign_index(longitude) == sign_idx
)
aspecting = sorted(
name for name, longitude in positions.items()
if sign_index(longitude) != sign_idx and sign_idx in _aspecting_signs(sign_index(longitude))
)
exalted = sorted(
name for name in occupants
if sign_idx in _EXALTED_SIGNS.get(name, set())
)
candidates[sign_idx] = {
"sign_index": sign_idx,
"sign": SIGNS[sign_idx],
"occupants": occupants,
"occupant_count": len(occupants),
"rasi_aspecting_planets": aspecting,
"rasi_aspect_count": len(aspecting),
"exalted_occupants": exalted,
}
second_score = (
candidates[second]["occupant_count"],
candidates[second]["rasi_aspect_count"],
bool(candidates[second]["exalted_occupants"]),
)
eighth_score = (
candidates[eighth]["occupant_count"],
candidates[eighth]["rasi_aspect_count"],
bool(candidates[eighth]["exalted_occupants"]),
)
payload = {
"profile": "pvr_niryaana_shoola_ayur_rasi_strength_v1",
"included_bodies": list(_AYUR_RASI_STRENGTH_BODIES),
"ascendant_sign_index": lagna,
"second_sign_index": second,
"eighth_sign_index": eighth,
"candidates": candidates,
"selection_levels": [
"occupant_count",
"all_planet_rasi_aspect_count",
"exalted_occupant_presence",
],
}
if second_score == eighth_score:
return {
**payload,
"status": "blocked",
"reason": "niryaana_shoola_ayur_rasi_strength_tie_requires_source_rule",
}
selected = second if second_score > eighth_score else eighth
return {
**payload,
"status": "selected",
"selected_sign_idx": selected,
"selected_sign": SIGNS[selected],
}
def niryaana_shoola_applicability(
*,
ascendant_longitude: float,
second_strength: float,
eighth_strength: float,
) -> dict:
"""Start from the stronger given second or eighth strength. Equal strength blocks."""
lagna = sign_index(float(ascendant_longitude))
second, eighth = (lagna + 1) % 12, (lagna + 7) % 12
second_strength, eighth_strength = float(second_strength), float(eighth_strength)
if second_strength == eighth_strength:
return {
"applicable": False,
"execution_status": "blocked",
"ascendant_sign_index": lagna,
"second_sign_index": second,
"eighth_sign_index": eighth,
"second_strength": second_strength,
"eighth_strength": eighth_strength,
"rule_family": "pvr_niryaana_shoola_2nd_8th_modality",
"reason": "The cited Niryaana Shoola rule does not provide a tie-break for equal second/eighth strength.",
}
start = second if second_strength > eighth_strength else eighth
return {
"applicable": True,
"ascendant_sign_index": lagna,
"second_sign_index": second,
"eighth_sign_index": eighth,
"second_strength": second_strength,
"eighth_strength": eighth_strength,
"starting_sign_index": start,
"starting_sign": SIGNS[start],
"direction": "forward" if start % 2 == 0 else "reverse",
"rule_family": "pvr_niryaana_shoola_2nd_8th_modality",
"reason": "Niryaana Shoola starts from the stronger second/eighth sign; odd seeds run forward and even seeds reverse.",
}
def niryaana_shoola_mahadashas(birth: date, start_sign: int) -> list[dict]:
"""Twelve mahadashas only. Odd signs go forward; even signs go backward."""
start_sign = int(start_sign) % 12
order = _forward(start_sign) if start_sign % 2 == 0 else _reverse(start_sign)
periods = []
cursor = birth
for sign_idx in order:
years = niryaana_shoola_years(sign_idx)
end = add_years(cursor, years)
periods.append(_period(sign_idx, years, cursor, end))
cursor = end
return periods
def _niryaana_shoola(birth: date, ascendant_longitude: float, planet_lons: Mapping[str, float]) -> dict:
picked = select_niryaana_shoola_start_sign(
ascendant_longitude=ascendant_longitude,
planet_longitudes=planet_lons,
)
if picked.get("status") != "selected":
return _omitted(
_NIRYAANA_PROFILE,
_NIRYAANA_SOURCE_ZH,
_NIRYAANA_SOURCE_EN,
"本轮不上表:二宫与八宫力量相同,原书没有给出进一步的裁决规则。",
"This report does not include Niryaana Shoola: the second and eighth signs are equal, and the source gives no further tie-break.",
)
start_sign = int(picked["selected_sign_idx"])
return _listed(
_NIRYAANA_PROFILE,
_NIRYAANA_SOURCE_ZH,
_NIRYAANA_SOURCE_EN,
niryaana_shoola_mahadashas(birth, start_sign),
direction="forward" if start_sign % 2 == 0 else "reverse",
strength_profile=picked["profile"],
)
def _nine_year(birth: date, lagna: int, planet_lons: Mapping[str, float], rashi: Mapping, profile: str, source_zh: str, source_en: str) -> dict:
seventh = (lagna + 6) % 12
picked = stronger_sign(lagna, seventh, rashi, planet_lons)
@@ -397,15 +573,7 @@ def calculate_rashi_dashas(birth: date | datetime | str, ascendant_longitude: fl
"Crux of Vedic Astrology, printed page 395",
),
"navamsha": _navamsha(birth_day, float(ascendant_longitude), longitudes),
"niryaana_shoola": _nine_year(
birth_day,
lagna,
longitudes,
rashi,
"goel_nirayana_shoola_v1",
"Predicting through Jaimini Astrology,印页 162",
"Predicting through Jaimini Astrology, printed page 162",
),
"niryaana_shoola": _niryaana_shoola(birth_day, float(ascendant_longitude), longitudes),
"lagna_kendradi": _omitted(
"",
"",