Bridge functional benefic malefic layer

This commit is contained in:
732642856
2026-06-28 18:18:33 +08:00
parent 7dc36ce558
commit 09817b96fd
8 changed files with 317 additions and 105 deletions
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Functional benefic/malefic classification by ascendant."""
from __future__ import annotations
from typing import Any
SIGNS = [
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
]
SIGN_TO_INDEX = {name: idx for idx, name in enumerate(SIGNS)}
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",
}
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
SOURCE = "strict_functional_benefic_malefic_v1"
def normalize_sign(value: Any) -> str | None:
if not isinstance(value, str):
return None
lowered = value.strip().lower()
for sign in SIGNS:
if sign.lower() == lowered:
return sign
return None
def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]:
"""Return functional benefics, malefics and ownership roles for a Lagna."""
asc_sign = normalize_sign(ascendant)
if asc_sign is None:
return {
"status": "blocked",
"ascendant": ascendant if isinstance(ascendant, str) else None,
"functional_benefics": [],
"functional_malefics": [],
"functional_neutrals": [],
"yogakarakas": [],
"owned_houses": {},
"effect_on_confidence": "Functional layer blocked: unknown ascendant sign.",
"source": SOURCE,
}
asc_idx = SIGN_TO_INDEX[asc_sign]
owned_houses: dict[str, list[int]] = {}
for house_num in range(1, 13):
sign = SIGNS[(asc_idx + house_num - 1) % 12]
lord = SIGN_LORDS.get(sign)
if lord:
owned_houses.setdefault(lord, []).append(house_num)
trines = {1, 5, 9}
kendras = {1, 4, 7, 10}
challenging = {3, 6, 8, 11, 12}
benefics: set[str] = set()
malefics: set[str] = set()
yogakarakas: set[str] = set()
neutrals: set[str] = set()
for planet in PLANETS:
houses = owned_houses.get(planet, [])
if not houses:
continue
owns_trine = any(house in trines for house in houses)
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"}:
yogakarakas.add(planet)
benefics.add(planet)
elif owns_trine:
benefics.add(planet)
elif owns_challenge and 1 not in houses:
malefics.add(planet)
elif owns_kendra and planet in {"Jupiter", "Venus", "Mercury", "Moon"}:
neutrals.add(planet)
else:
neutrals.add(planet)
eighth_lord = next((planet for planet, houses in owned_houses.items() if 8 in houses), None)
if eighth_lord in {"Sun", "Moon"} and eighth_lord in malefics:
malefics.remove(eighth_lord)
neutrals.add(eighth_lord)
return {
"status": "used",
"ascendant": asc_sign,
"functional_benefics": sorted(benefics),
"functional_malefics": sorted(malefics),
"functional_neutrals": sorted(neutrals - benefics - malefics),
"yogakarakas": sorted(yogakarakas),
"owned_houses": {planet: houses for planet, houses in sorted(owned_houses.items())},
"effect_on_confidence": (
"高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;"
"若功能属性与自然属性冲突,应降低置信度或显式标记冲突。"
),
"source": SOURCE,
}
+47 -16
View File
@@ -9,6 +9,7 @@
import argparse
import base64
import html as html_lib
import io
import json, sys, os, math
import importlib.util
@@ -441,6 +442,42 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
raise BadRequest('report html cannot include active content')
return html
def _inject_functional_benefic_malefic_summary(self, html, snapshot):
if not isinstance(snapshot, dict):
return html
if snapshot.get('status') in {None, 'blocked', 'not_used'}:
return html
benefics = snapshot.get('functional_benefics')
malefics = snapshot.get('functional_malefics')
if not isinstance(benefics, list) or not isinstance(malefics, list):
return html
def _escape(value):
return html_lib.escape(str(value or ''))
ascendant = _escape(snapshot.get('ascendant') or snapshot.get('asc_sign') or 'Unknown')
benefic_text = _escape(', '.join(str(item) for item in benefics) or 'None')
malefic_text = _escape(', '.join(str(item) for item in malefics) or 'None')
confidence_text = _escape(snapshot.get('effect_on_confidence') or 'Functional role layer was used in the final judgement.')
source_text = _escape(snapshot.get('source') or 'strict_functional_benefic_malefic_v1')
summary = (
'<section data-functional-role-summary="true" '
'style="margin:24px 0;padding:16px;border:1px solid #d9dde8;border-radius:8px;'
'background:#f7f9fc;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;">'
'<h2 style="margin:0 0 12px;font-size:20px;">Functional Benefic/Malefic</h2>'
f'<p style="margin:0 0 8px;"><strong>Ascendant:</strong> {ascendant}</p>'
f'<p style="margin:0 0 8px;"><strong>Functional Benefics:</strong> {benefic_text}</p>'
f'<p style="margin:0 0 8px;"><strong>Functional Malefics:</strong> {malefic_text}</p>'
f'<p style="margin:0 0 8px;"><strong>Confidence Impact:</strong> {confidence_text}</p>'
f'<p style="margin:0;color:#5b6472;font-size:13px;"><strong>Source:</strong> {source_text}</p>'
'</section>'
)
body_close = re.search(r'</body\s*>', html, re.IGNORECASE)
if body_close:
return html[:body_close.start()] + summary + html[body_close.start():]
return html + summary
def _artifact_base64(self, path):
size = os.path.getsize(path)
if size > MAX_REPORT_BASE64_BYTES:
@@ -505,6 +542,10 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
def _compute_report_artifact(self, body):
html = self._validate_report_html(body.get('html'))
html = self._inject_functional_benefic_malefic_summary(
html,
body.get('functional_benefic_malefic'),
)
fmt = body.get('format', 'html')
if fmt not in {'html', 'pdf'}:
raise BadRequest('format must be html or pdf')
@@ -1921,30 +1962,20 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
def _functional_benefic_malefic_snapshot(self, planets, ascendant):
try:
from yoga_engine import YogaContext
from functional_benefics import derive_functional_benefic_malefic
asc_sign = ascendant.get('sign')
if not asc_sign or not isinstance(planets, dict):
raise ValueError('missing ascendant sign or planets')
context = YogaContext(planets, asc_sign)
benefics = context.functional_benefics()
malefics = context.functional_malefics()
return {
'status': 'used',
'ascendant': asc_sign,
'functional_benefics': benefics,
'functional_malefics': malefics,
'effect_on_confidence': (
'高严谨模式下必须叠加功能性吉凶星;若与自然吉凶属性冲突,'
'应降低置信度并在 Technique Audit Table 中显式说明。'
),
}
return derive_functional_benefic_malefic(asc_sign)
except Exception as exc:
return {
'status': 'blocked',
'ascendant': ascendant.get('sign'),
'functional_benefics': [],
'functional_malefics': [],
'functional_neutrals': [],
'yogakarakas': [],
'owned_houses': {},
'effect_on_confidence': f'未完成功能性吉凶星判定,需降低高严谨结论置信度: {exc}',
'source': 'strict_functional_benefic_malefic_v1',
}
def _detect_yogas(self, planets, asc_idx):
+6 -16
View File
@@ -859,30 +859,20 @@ def _oracle_progress_snapshot():
def _functional_benefic_malefic_snapshot(planets, ascendant):
try:
from yoga_engine import YogaContext
from functional_benefics import derive_functional_benefic_malefic
asc_sign = ascendant.get('sign') if isinstance(ascendant, dict) else None
if not asc_sign or not isinstance(planets, dict):
raise ValueError('missing ascendant sign or planets')
context = YogaContext(planets, asc_sign)
benefics = context.functional_benefics()
malefics = context.functional_malefics()
return {
'status': 'used',
'ascendant': asc_sign,
'functional_benefics': benefics,
'functional_malefics': malefics,
'effect_on_confidence': (
'高严谨模式下必须叠加功能性吉凶星;若与自然吉凶属性冲突,'
'应降低置信度并在 Technique Audit Table 中显式说明。'
),
}
return derive_functional_benefic_malefic(asc_sign)
except Exception as exc:
return {
'status': 'blocked',
'ascendant': ascendant.get('sign') if isinstance(ascendant, dict) else None,
'functional_benefics': [],
'functional_malefics': [],
'functional_neutrals': [],
'yogakarakas': [],
'owned_houses': {},
'effect_on_confidence': f'未完成功能性吉凶星判定,需降低高严谨结论置信度: {exc}',
'source': 'strict_functional_benefic_malefic_v1',
}
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""CLI wrapper for functional benefic/malefic classification."""
from __future__ import annotations
import argparse
import json
from typing import Any
from functional_benefics import PLANETS, derive_functional_benefic_malefic
def _render_text(report: dict[str, Any]) -> str:
lines = [
"=== Oracle stdout ===",
f"Ascendant: {report.get('ascendant')}",
"--------------------------------------------------",
]
owned_houses = report.get("owned_houses", {})
benefics = set(report.get("functional_benefics", []))
malefics = set(report.get("functional_malefics", []))
yogakarakas = set(report.get("yogakarakas", []))
for planet in PLANETS:
owned = owned_houses.get(planet, [])
houses = " & ".join(map(str, owned)) if owned else "-"
if planet in yogakarakas:
lines.append(f"{planet}: Yogakaraka (Lord of {houses}) -> Highly Auspicious")
elif planet in benefics:
lines.append(f"{planet}: Functional Benefic (Lord of {houses}) -> Auspicious")
elif planet in malefics:
lines.append(f"{planet}: Functional Malefic (Lord of {houses}) -> Destructive / Obstacle")
else:
lines.append(f"{planet}: Neutral / Mixed (Lord of {houses}) -> Depends on placement")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description="Oracle CLI: Functional Benefic/Malefic Determiner")
parser.add_argument("--ascendant", required=True, type=str, help="The ascendant sign, e.g. Leo")
parser.add_argument("--format", choices=["text", "json"], default="text")
args = parser.parse_args()
report = derive_functional_benefic_malefic(args.ascendant)
if args.format == "json":
print(json.dumps(report, ensure_ascii=False, indent=2))
else:
print(_render_text(report))
return 0 if report.get("status") == "used" else 1
if __name__ == "__main__":
raise SystemExit(main())