From 09817b96fdf120dd5ee63359cf966cd0204a3dfb Mon Sep 17 00:00:00 2001
From: 732642856 <732642856@qq.com>
Date: Sun, 28 Jun 2026 18:18:33 +0800
Subject: [PATCH] Bridge functional benefic malefic layer
---
...benefic_malefic_bridge_audit_2026_06_28.md | 42 +++++++
mcp_server.py | 80 ++-----------
scripts/functional_benefics.py | 112 ++++++++++++++++++
scripts/jyotish_api_server.py | 63 +++++++---
scripts/jyotish_engine.py | 22 +---
scripts/oracle_functional_benefics.py | 52 ++++++++
tests/test_api_server_security.py | 25 ++++
...st_mcp_strict_workflow_functional_layer.py | 26 ++++
8 files changed, 317 insertions(+), 105 deletions(-)
create mode 100644 docs/research/functional_benefic_malefic_bridge_audit_2026_06_28.md
create mode 100644 scripts/functional_benefics.py
create mode 100644 scripts/oracle_functional_benefics.py
diff --git a/docs/research/functional_benefic_malefic_bridge_audit_2026_06_28.md b/docs/research/functional_benefic_malefic_bridge_audit_2026_06_28.md
new file mode 100644
index 00000000..18f509eb
--- /dev/null
+++ b/docs/research/functional_benefic_malefic_bridge_audit_2026_06_28.md
@@ -0,0 +1,42 @@
+# Functional Benefic/Malefic Bridge Audit - 2026-06-28
+
+## Scope
+
+This pass promotes the previously untracked `scripts/oracle_functional_benefics.py`
+fragment into a reusable strict-workflow layer.
+
+## What Changed
+
+- Added `/Users/wuyongnaren/Documents/印度占星/scripts/functional_benefics.py` as the single source for functional benefic/malefic classification by Lagna.
+- Converted `/Users/wuyongnaren/Documents/印度占星/scripts/oracle_functional_benefics.py` into a CLI wrapper around that module.
+- Routed MCP strict workflows through the shared module.
+- Routed full-reading prompt-pack and API prompt-pack snapshots through the same shared module.
+
+## Contract
+
+The bridge requires only a valid ascendant sign.
+
+It returns:
+
+- `status`
+- `ascendant`
+- `functional_benefics`
+- `functional_malefics`
+- `functional_neutrals`
+- `yogakarakas`
+- `owned_houses`
+- `effect_on_confidence`
+- `source`
+
+## Boundary
+
+- This layer classifies functional house-lord roles.
+- It does not replace natural benefic/malefic assessment.
+- It does not directly force event labels.
+- It must appear in strict workflow evidence and Technique Audit Table outputs for high-rigor readings.
+
+## Regression Coverage
+
+- `/Users/wuyongnaren/Documents/印度占星/tests/test_mcp_strict_workflow_functional_layer.py`
+- `/Users/wuyongnaren/Documents/印度占星/tests/test_cli_smoke.py`
+- `/Users/wuyongnaren/Documents/印度占星/tests/test_api_server_security.py::test_chart_ai_prompt_pack_exposes_functional_benefic_malefic_layer`
diff --git a/mcp_server.py b/mcp_server.py
index ef879784..d7f7f303 100644
--- a/mcp_server.py
+++ b/mcp_server.py
@@ -34,6 +34,7 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(SCRIPT_DIR, "scripts"))
from mcp.server.fastmcp import FastMCP
+from functional_benefics import derive_functional_benefic_malefic
# ============================================================================
# MCP Server
@@ -452,86 +453,19 @@ def _derive_external_activation_support(modules: Dict[str, Any], domain: str) ->
def _derive_functional_benefic_malefic(modules: Dict[str, Any]) -> Dict[str, Any]:
chart = _safe_get(modules, "chart")
ascendant = _safe_get(chart, "ascendant") if isinstance(chart, dict) else None
- planets = _safe_get(chart, "planets") if isinstance(chart, dict) else None
- if not isinstance(ascendant, dict) or not isinstance(planets, dict):
+ if not isinstance(ascendant, dict):
return {
"status": "blocked",
"ascendant": ascendant.get("sign") if isinstance(ascendant, dict) else None,
"functional_benefics": [],
"functional_malefics": [],
- "effect_on_confidence": "Missing chart.ascendant or chart.planets; functional layer blocked.",
+ "functional_neutrals": [],
+ "yogakarakas": [],
+ "owned_houses": {},
+ "effect_on_confidence": "Missing chart.ascendant; functional layer blocked.",
"source": "strict_functional_benefic_malefic_v1",
}
- asc_sign = ascendant.get("sign")
- asc_idx = _SIGN_TO_INDEX.get(asc_sign)
- if asc_idx is None:
- return {
- "status": "blocked",
- "ascendant": asc_sign,
- "functional_benefics": [],
- "functional_malefics": [],
- "effect_on_confidence": "Functional layer blocked: unknown ascendant sign.",
- "source": "strict_functional_benefic_malefic_v1",
- }
-
- 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 ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"):
- 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)
-
- # Classical softening: Sun/Moon as 8L are not treated as harshly as other 8L.
- eighth_lord = owned_houses and 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": (
- "High-rigor outputs must combine functional house-lord roles with natural roles; "
- "conflicts should cap confidence or be explicitly noted."
- ),
- "source": "strict_functional_benefic_malefic_v1",
- }
+ return derive_functional_benefic_malefic(ascendant.get("sign"))
def _derive_synastry_relationship_support(modules: Dict[str, Any]) -> Dict[str, Any]:
diff --git a/scripts/functional_benefics.py b/scripts/functional_benefics.py
new file mode 100644
index 00000000..eaf973e6
--- /dev/null
+++ b/scripts/functional_benefics.py
@@ -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,
+ }
diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py
index b01700f6..c42ed5a1 100644
--- a/scripts/jyotish_api_server.py
+++ b/scripts/jyotish_api_server.py
@@ -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 = (
+ ' Ascendant: {ascendant} Functional Benefics: {benefic_text} Functional Malefics: {malefic_text} Confidence Impact: {confidence_text} Source: {source_text}Functional Benefic/Malefic
'
+ f'