From b78330ae70cf78714e835df888aa508dc85678b2 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Mon, 29 Jun 2026 09:58:28 +0800 Subject: [PATCH] Expose VedAstro overview in exports and life graphs --- jyotish-app/export.js | 33 ++++++ mcp_server.py | 147 ++++++++++++++++++++++++++ tests/test_frontend_productization.py | 4 + tests/test_life_event_graph_v1.py | 50 +++++++++ 4 files changed, 234 insertions(+) diff --git a/jyotish-app/export.js b/jyotish-app/export.js index 25539a27..15ff921f 100644 --- a/jyotish-app/export.js +++ b/jyotish-app/export.js @@ -607,6 +607,8 @@ function _buildHTMLReport(chartData, extras) { ${_vimsopakaSemanticSummarySection(vimsopakaSemanticSummary)} + ${_vedastroOverviewSection(vedastroOverview)} + ${_techniqueAuditTableSection(techniqueAuditTable)} ${_relationshipStrictNarrativeSection(relationshipNarrative)} @@ -692,6 +694,37 @@ function _techniqueAuditTableSection(rows) { `; } +function _vedastroOverviewSection(overview) { + if (!overview || typeof overview !== 'object') return ''; + const domainStatuses = overview.domain_statuses && typeof overview.domain_statuses === 'object' + ? Object.entries(overview.domain_statuses).map(([domain, status]) => `${domain}:${status}`).join(';') + : '—'; + const topEvents = overview.top_events_by_domain && typeof overview.top_events_by_domain === 'object' + ? Object.entries(overview.top_events_by_domain).map(([domain, payload]) => { + const signal = payload?.signal_label || payload?.event_id || '—'; + const start = payload?.start || '—'; + return `${domain}:${signal} @ ${start}`; + }).join(';') + : '—'; + return `
+

VedAstro 概览证据

+
+

这是主入口自动附带的 VedAstro 单日概览,用来补充外部雷达视角;它是 overview only,不替代长周期精扫。

+ ${_table([ + ['状态', overview.status || 'blocked'], + ['Source', overview.source || 'vedastro_service_adapter_candidate'], + ['Ingestion', overview.ingestion_profile || '-'], + ['Scope', overview.search_scope || 'single_day_overview'], + ['Reference Date', overview.reference_date || '-'], + ['Event Count', overview.event_count == null ? '0' : String(overview.event_count)], + ['Domain Statuses', domainStatuses || '—'], + ['Top Events', topEvents || '—'], + ['Boundary', overview.boundary_note || 'overview only;不替代长周期精扫。'], + ])} +
+
`; +} + function _relationshipStrictNarrativeSection(narrative) { if (!narrative || typeof narrative !== 'object') return ''; const strengths = Array.isArray(narrative.strengths) ? narrative.strengths : []; diff --git a/mcp_server.py b/mcp_server.py index e6e8cbd0..6e639c15 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -27,15 +27,21 @@ import os import json import subprocess import asyncio +from copy import deepcopy +from datetime import datetime, timedelta from typing import Dict, Any, Optional, List # Add scripts dir to path so imports work SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(SCRIPT_DIR, "scripts")) +from local_env import load_local_env + from mcp.server.fastmcp import FastMCP from functional_benefics import derive_functional_benefic_malefic +load_local_env(SCRIPT_DIR) + # ============================================================================ # MCP Server # ============================================================================ @@ -144,6 +150,11 @@ _NAKSHATRA_LORDS = [ _WEALTH_HOUSES = {2, 5, 9, 10, 11} _NAKSHATRA_SPAN = 360.0 / 27.0 _SHADBALA_REQUIRED_COMPONENTS = ["sthana", "dig", "kala", "chesta", "naisargika", "drik"] +_VEDASTRO_ROUTE_DOMAIN = { + "career": "career", + "relationship": "marriage", + "finance": "wealth", +} def _normalize_longitude(value: Any) -> Optional[float]: @@ -438,6 +449,7 @@ def _derive_external_activation_support(modules: Dict[str, Any], domain: str) -> "required": True, "operation": "range_scan", "external_calculation_coverage": "VedAstro 596+/600+ calculation nodes", + "provenance": provenance, "reason": ( "VedAstro EventsAtRange / FindLifeEvents-style high-frequency radar " "was not provided; keep timing confidence bounded." @@ -902,6 +914,8 @@ def _lord_for_house_from_lagna(asc_sign: Optional[str], house_num: int) -> Optio def _extract_dignity_code(status: str) -> Optional[str]: if "Neecha Bhanga" in status or "落陷取消" in status: return "NEECHA_BHANGA" + if "Great Friend" in status or "极友" in status: + return "GREAT_FRIEND" if "Great Enemy" in status or "极敌" in status: return "GREAT_ENEMY" return None @@ -957,6 +971,7 @@ def _derive_dignity_guardrail(route: str, present: Dict[str, Any]) -> Dict[str, return base supportive_hits: List[str] = [] + supportive_friend_hits: List[str] = [] friction_hits: List[str] = [] for planet_name, pdata in planets.items(): @@ -974,6 +989,7 @@ def _derive_dignity_guardrail(route: str, present: Dict[str, Any]) -> Dict[str, dignity_code = _extract_dignity_code(status) effect = ( "supportive_recovery" if dignity_code == "NEECHA_BHANGA" + else "supportive_friendship" if dignity_code == "GREAT_FRIEND" else "high_friction" if dignity_code == "GREAT_ENEMY" else "none" ) @@ -986,6 +1002,8 @@ def _derive_dignity_guardrail(route: str, present: Dict[str, Any]) -> Dict[str, }) if dignity_code == "NEECHA_BHANGA": supportive_hits.append(planet_name) + elif dignity_code == "GREAT_FRIEND": + supportive_friend_hits.append(planet_name) elif dignity_code == "GREAT_ENEMY": friction_hits.append(planet_name) @@ -999,6 +1017,16 @@ def _derive_dignity_guardrail(route: str, present: Dict[str, Any]) -> Dict[str, elif supportive_hits: base["status"] = "caution" base["score_delta"] = 5 + elif supportive_friend_hits and friction_hits: + base["status"] = "conflict" + base["conflict_flags"] = [ + "great_friend_on_key_significator", + "great_enemy_on_key_significator", + ] + base["score_delta"] = 0 + elif supportive_friend_hits: + base["status"] = "caution" + base["score_delta"] = 3 elif friction_hits: base["status"] = "caution" base["score_delta"] = -5 @@ -1196,6 +1224,8 @@ def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[s secondary_context.append("dignity_conflict") elif dignity_guardrail.get("score_delta") == 5: secondary_context.append("dignity_supportive_recovery") + elif dignity_guardrail.get("score_delta") == 3: + secondary_context.append("dignity_supportive_friendship") elif dignity_guardrail.get("score_delta") == -5: secondary_context.append("dignity_high_friction") @@ -1354,6 +1384,8 @@ def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[s secondary_context.append("dignity_conflict") elif dignity_guardrail.get("score_delta") == 5: secondary_context.append("dignity_supportive_recovery") + elif dignity_guardrail.get("score_delta") == 3: + secondary_context.append("dignity_supportive_friendship") elif dignity_guardrail.get("score_delta") == -5: secondary_context.append("dignity_high_friction") return { @@ -1456,6 +1488,18 @@ def _build_life_event_graph(route: str, strict: Dict[str, Any]) -> Dict[str, Any external_activation = present.get("external_activation") if isinstance(external_activation, dict): + provenance = external_activation.get("provenance") + if isinstance(provenance, dict) and provenance.get("ingestion_profile") == "main_entry_overview": + nodes.append( + { + "kind": "external_overview", + "label": "VedAstro main-entry overview", + "ingestion_profile": provenance.get("ingestion_profile"), + "search_scope": provenance.get("search_scope"), + "reference_date": provenance.get("reference_date"), + "source": external_activation.get("source"), + } + ) for event in external_activation.get("events") or []: if not isinstance(event, dict): continue @@ -1747,6 +1791,95 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An }) +def _default_vedastro_scan_window(transit_date: str) -> tuple[str, str]: + try: + start = datetime.strptime(str(transit_date), "%Y-%m-%d").date() + except ValueError: + return str(transit_date), str(transit_date) + end = start + timedelta(days=180) + return start.isoformat(), end.isoformat() + + +def _maybe_attach_vedastro_evidence( + route: str, + result: Dict[str, Any], + *, + year: int, + month: int, + day: int, + hour: int, + minute: int, + lat: float, + lon: float, + tz: float, + transit_date: str, + node_mode: str, +) -> Dict[str, Any]: + if not isinstance(result, dict) or result.get("error"): + return result + + modules = result.get("modules") + if not isinstance(modules, dict): + return result + + vedastro_domain = _VEDASTRO_ROUTE_DOMAIN.get(route) + if not vedastro_domain: + return result + + if modules.get("vedastro_range_scan_result") or _safe_get(modules, "external_activation", "evidence_ledger"): + return result + + try: + from vedastro_service_adapter import run_range_scan_for_case + except Exception: + return result + + start_date, end_date = _default_vedastro_scan_window(transit_date) + case = { + "year": year, + "month": month, + "day": day, + "hour": hour, + "minute": minute, + "second": 0, + "lat": lat, + "lon": lon, + "tz": tz, + "ayanamsa_policy": ( + _safe_get(result, "meta", "ayanamsa") + or _safe_get(result, "chart", "ayanamsa") + or "lahiri" + ), + "node_policy": node_mode or "mean", + } + scan_result = run_range_scan_for_case( + case, + vedastro_domain, + start_date, + end_date, + case_id=f"strict_workflow_{route}", + ) + if not isinstance(scan_result, dict): + return result + + attached_scan = deepcopy(scan_result) + metadata = attached_scan.get("source_metadata") + if not isinstance(metadata, dict): + metadata = {} + metadata.setdefault("auto_ingested_by", "strict_workflow") + metadata.setdefault("strict_route", route) + metadata.setdefault("scan_window", {"start_date": start_date, "end_date": end_date}) + metadata.setdefault("adapter_status", attached_scan.get("status")) + if attached_scan.get("reason"): + metadata.setdefault("adapter_reason", attached_scan.get("reason")) + attached_scan["source_metadata"] = metadata + + enriched = dict(result) + enriched["modules"] = dict(modules) + enriched["modules"]["vedastro_range_scan_result"] = attached_scan + return enriched + + # ============================================================================ # Tools # ============================================================================ @@ -2240,6 +2373,20 @@ def strict_workflow( }) if isinstance(result, dict) and "error" not in result: + result = _maybe_attach_vedastro_evidence( + route, + result, + year=year, + month=month, + day=day, + hour=hour, + minute=minute, + lat=lat, + lon=lon, + tz=tz, + transit_date=transit_date, + node_mode=node_mode, + ) strict_evidence = _collect_strict_evidence(route, result) result["routing"] = { "question_type": route, diff --git a/tests/test_frontend_productization.py b/tests/test_frontend_productization.py index 987e825b..5f11dfe8 100644 --- a/tests/test_frontend_productization.py +++ b/tests/test_frontend_productization.py @@ -1943,9 +1943,13 @@ def test_provenance_panchanga_workspace_panel_is_productized() -> None: assert "_functionalRoleSummarySection" in export_js assert "_techniqueAuditTableSection" in export_js assert "_vimsopakaSemanticSummarySection" in export_js + assert "_vedastroOverviewSection" in export_js assert "Functional Benefic/Malefic" in export_js assert "Technique Audit Table" in export_js assert "Vimsopaka 语义摘要" in export_js + assert "VedAstro 概览证据" in export_js + assert "single_day_overview" in export_js + assert "不替代长周期精扫" in export_js assert "highlights" in export_js assert "warnings" in export_js assert "vimsopaka_semantic_summary:" in main diff --git a/tests/test_life_event_graph_v1.py b/tests/test_life_event_graph_v1.py index 74d297dc..f0de5aea 100644 --- a/tests/test_life_event_graph_v1.py +++ b/tests/test_life_event_graph_v1.py @@ -212,3 +212,53 @@ def test_strict_workflow_accepts_adapter_range_scan_result_without_manual_repack assert external["source"] == "vedastro_service_adapter_candidate" assert external["provenance"]["request_hash"] == "a" * 64 assert any(node["kind"] == "external_window" for node in strict["life_event_graph"]["event_nodes"]) + + +def test_life_event_graph_surfaces_main_entry_overview_boundary_node() -> None: + strict = { + "event_judgement": { + "event_family": "relationship", + "score": 74, + "verdict": "moderate_probability_window", + "dominant_label": "legal_marriage", + "secondary_context": ["external_activation_support"], + }, + "present_evidence": { + "external_activation": { + "level": "moderate", + "source": "vedastro_service_adapter_candidate", + "signals": ["vedastro_range_scan"], + "events": [ + { + "event_id": "GocharJupiterIn7th", + "signal_key": "gochar_jupiter_7th_marriage", + "signal_label": "Jupiter in 7th marriage window", + "signal_family": "marriage_trigger", + "score": 72, + "start": "2026-06-29", + "end": "2026-06-29", + "tags": ["marriage", "transit"], + } + ], + "provenance": { + "ingestion_profile": "main_entry_overview", + "search_scope": "single_day_overview", + "reference_date": "2026-06-29", + }, + } + }, + "confidence_cap": "medium", + "missing_evidence": [], + "blocked": False, + } + + graph = _build_life_event_graph("relationship", strict) + + assert { + "kind": "external_overview", + "label": "VedAstro main-entry overview", + "ingestion_profile": "main_entry_overview", + "search_scope": "single_day_overview", + "reference_date": "2026-06-29", + "source": "vedastro_service_adapter_candidate", + } in graph["event_nodes"]