feat(functional): adopt upstream BPHS ch.34 profile v2; fix Pancha Mahapurusha self-conjunction; yoga engine reads the same functional roles

Ported verbatim from the upstream skill repo (yinduzhanxing e9beae6b) with
product authorization 2026-10-02: scripts/functional_benefics.py,
scripts/pancha_mahapurusha.py, references/functional-role-profile.md; only
the two functional-role methods of scripts/yoga_engine.py (our file diverged).
v5 77-case rectification eval: 5 cells up (to +7.8pp), one cell -1.3pp (one
case); product accepted. Rectification memoization golden still to be
regenerated (scores change with the grouping).

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:
Jesse_Chen
2026-10-02 10:57:14 +08:00
co-authored by Claude Opus 5.5
parent f85794661b
commit 57782aea8a
5 changed files with 117 additions and 60 deletions
+41 -13
View File
@@ -26,16 +26,22 @@ SIGN_LORDS = {
"Pisces": "Jupiter",
}
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
# v2 (2026-10-02, BUG-1158 partial, product decision on
# TASK-consult-career-yoga-functional-20261002 T1): a yogakaraka must own one
# kendra among 4 / 7 / 10 AND one trikona among 5 / 9 (BPHS ch.34 v13); the
# 1st house no longer counts for this test, so a lagna lord is not a
# yogakaraka by owning the 1st. The benefic / malefic / neutral grouping is
# unchanged from v1. The full per-lagna BPHS ch.34 table waits for an
# astrology consultant on branch codex/consult-career-yoga-functional-t1-20261002.
SOURCE = "strict_functional_benefic_malefic_v2"
YOGAKARAKA_KENDRAS = frozenset({4, 7, 10})
YOGAKARAKA_TRIKONAS = frozenset({5, 9})
# General ownership rules, with explicit sign-specific statements taking
# precedence. Neutral means mixed/association-dependent, not harmless.
PROFILE = "bphs_ch34_general_with_sign_exceptions_v2"
ROLE_OVERRIDES = {
"Aries": {"Mars": "neutral"},
"Taurus": {"Venus": "malefic", "Sun": "benefic"},
"Gemini": {"Jupiter": "malefic"},
"Virgo": {"Jupiter": "malefic"},
"Libra": {"Venus": "neutral", "Mars": "malefic"},
"Scorpio": {"Mars": "neutral", "Sun": "benefic"},
"Sagittarius": {"Jupiter": "neutral", "Mercury": "neutral"},
"Capricorn": {"Moon": "malefic"},
"Aquarius": {"Mars": "malefic", "Mercury": "neutral"},
"Pisces": {"Mercury": "malefic"},
}
def normalize_sign(value: Any) -> str | None:
@@ -74,7 +80,7 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]:
trines = {1, 5, 9}
kendras = {1, 4, 7, 10}
challenging = {3, 6, 8, 11, 12}
challenging = {3, 6, 8, 11}
benefics: set[str] = set()
malefics: set[str] = set()
yogakarakas: set[str] = set()
@@ -88,9 +94,10 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]:
owns_kendra = any(house in kendras for house in houses)
owns_challenge = any(house in challenging for house in houses)
if owns_trine and owns_kendra and planet not in {"Sun", "Moon"}:
if YOGAKARAKA_KENDRAS & set(houses) and YOGAKARAKA_TRIKONAS & set(houses):
yogakarakas.add(planet)
# House 1 remains both angular and trinal, but cannot be reused as
# two distinct lordships for this strict Yoga-karaka field.
if set(houses) & {4, 7, 10} and set(houses) & {5, 9}:
yogakarakas.add(planet)
benefics.add(planet)
elif owns_trine:
benefics.add(planet)
@@ -106,6 +113,12 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]:
malefics.remove(eighth_lord)
neutrals.add(eighth_lord)
for planet, role in ROLE_OVERRIDES.get(asc_sign, {}).items():
benefics.discard(planet)
malefics.discard(planet)
neutrals.discard(planet)
{"benefic": benefics, "malefic": malefics, "neutral": neutrals}[role].add(planet)
return {
"status": "used",
"ascendant": asc_sign,
@@ -119,4 +132,19 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]:
"若功能属性与自然属性冲突,应降低置信度或显式标记冲突。"
),
"source": SOURCE,
"profile": PROFILE,
"lagna_lord": SIGN_LORDS[asc_sign],
"yogakaraka_definition": "distinct lordships: one of 4/7/10 and one of 5/9",
"rule_reference": "references/functional-role-profile.md",
"assertion_label": "parameter_sensitive",
"role_basis": {
planet: "BPHS ch34 sign-specific exception" if planet in ROLE_OVERRIDES.get(asc_sign, {})
else "BPHS ch34 general ownership; associations not evaluated"
for planet in PLANETS
},
"conditions": [
"Neutral/mixed is association-dependent, not an absence of risk.",
"Natural nature, strength, maraka roles and dasha activation remain separate.",
"Strict yogakarakas do not exhaust the text's broader yoga-producing associations.",
],
}
+3 -3
View File
@@ -64,11 +64,11 @@ def _is_combust(planet: str, planet_degree: float, sun_degree: float) -> bool:
return diff <= orb
def _has_malefic_aspect(planet_sign: str, all_planets: Dict) -> List[str]:
def _has_malefic_aspect(planet_sign: str, all_planets: Dict, planet: str = None) -> List[str]:
"""检查是否有凶星合相/相位该行星"""
afflicting = []
for pn, pd in all_planets.items():
if pn == planet_sign or pn not in NATURAL_MALEFICS:
if pn == planet or pn not in NATURAL_MALEFICS:
continue
# 简单合相检查
if pd.get('sign') == planet_sign:
@@ -124,7 +124,7 @@ def detect_pancha_mahapurusha(planets: Dict, sun_degree: float = None) -> List[D
is_valid = False
# 3. 凶星受克
malefics = _has_malefic_aspect(sign, planets)
malefics = _has_malefic_aspect(sign, planets, planet)
if malefics:
cancellations.append(f"{planet}受凶星合相: {', '.join(malefics)} — Yoga部分失效")
is_valid = False
+13 -30
View File
@@ -391,38 +391,21 @@ class YogaContext:
# --- Functional Malefic / Benefic(基于月亮盈亏和宫主关系) ---
def functional_malefics(self) -> List[str]:
"""
Return functional malefics for this chart.
Rules (per PyJHora/B.V. Raman):
- Lords of 3, 6, 8, 11, 12 are functional malefics
- Mercury is malefic if conjunct with malefic
- Moon is malefic if waning (Krishna Paksha)
"""
fm = []
dusthana_lords = [self.lord_of_house(h) for h in [3, 6, 8, 11, 12]]
fm.extend([l for l in dusthana_lords if l])
# Mercury becomes functional malefic if conjunct with natural malefic
if 'Mercury' in self.planets:
merc_h = self.house_of('Mercury')
merc_conj_malefic = any(
m in self.planets and self.house_of(m) == merc_h
for m in ['Mars', 'Saturn', 'Rahu', 'Ketu', 'Sun']
)
if merc_conj_malefic and 'Mercury' not in fm:
fm.append('Mercury')
# Waning Moon is functional malefic
if self.is_waning_moon() and 'Moon' in self.planets and 'Moon' not in fm:
fm.append('Moon')
return list(set(fm))
"""Ownership roles only; natural nature is a separate rule predicate."""
try:
from .functional_benefics import derive_functional_benefic_malefic
except ImportError:
from functional_benefics import derive_functional_benefic_malefic
return derive_functional_benefic_malefic(self.ascendant)['functional_malefics']
def functional_benefics(self) -> List[str]:
"""Return functional benefics = all planets minus functional malefics minus natural malefics."""
fm = set(self.functional_malefics())
nm = set(MALEFICS)
return [p for p in ALL_PLANETS if p in self.planets and p not in fm and p not in nm]
"""Use the same named ownership profile as the API and report."""
try:
from .functional_benefics import derive_functional_benefic_malefic
except ImportError:
from functional_benefics import derive_functional_benefic_malefic
benefics = derive_functional_benefic_malefic(self.ascendant)['functional_benefics']
return [p for p in benefics if p in self.planets]
# --- Shashtiamsa / Vaiseshikamsa 简化评分 ---
def is_shashtiamsa_evil(self, planet: str) -> bool: