From c0f78bd5e4adb9c625c8cf6dd31f838c16f4b672 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sun, 28 Jun 2026 16:42:33 +0800 Subject: [PATCH] Add life event graph v1 to strict workflows --- docs/research/ACTIVE_FRONTS.md | 1 + .../life_event_graph_v1_audit_2026_06_28.md | 51 ++++++ mcp_server.py | 165 +++++++++++++++++- tests/test_life_event_graph_v1.py | 137 +++++++++++++++ 4 files changed, 346 insertions(+), 8 deletions(-) create mode 100644 docs/research/life_event_graph_v1_audit_2026_06_28.md create mode 100644 tests/test_life_event_graph_v1.py diff --git a/docs/research/ACTIVE_FRONTS.md b/docs/research/ACTIVE_FRONTS.md index e30ab2d7..74b4a4f6 100644 --- a/docs/research/ACTIVE_FRONTS.md +++ b/docs/research/ACTIVE_FRONTS.md @@ -42,6 +42,7 @@ This file is the small index for the current engineering fronts that still drive - `/Users/wuyongnaren/Documents/印度占星/docs/research/vedastro_parity_matrix_latest.md` - `/Users/wuyongnaren/Documents/印度占星/docs/research/vedastro_parity_matrix_latest.json` +- `/Users/wuyongnaren/Documents/印度占星/docs/research/life_event_graph_v1_audit_2026_06_28.md` - `/Users/wuyongnaren/Documents/印度占星/scripts/vedastro_service_adapter.py` - follow only after the Jaimini marriage bridge v1 regression loop is closed - Use the parity matrix before adding or claiming VedAstro-equivalent capability. diff --git a/docs/research/life_event_graph_v1_audit_2026_06_28.md b/docs/research/life_event_graph_v1_audit_2026_06_28.md new file mode 100644 index 00000000..28ef6dcc --- /dev/null +++ b/docs/research/life_event_graph_v1_audit_2026_06_28.md @@ -0,0 +1,51 @@ +# Life Event Graph v1 Audit - 2026-06-28 + +## Scope + +`life_event_graph_v1` is a compact graph contract added to strict workflow outputs. It is not a high-frequency probability curve yet; it is the first product-ready ledger-to-graph bridge for front-end rendering and future VedAstro range-scan overlays. + +## Output Contract + +Each strict workflow response now includes: + +```json +{ + "life_event_graph": { + "version": "life_event_graph_v1", + "route": "relationship | career | finance | ...", + "dominant_label": "string | null", + "verdict": "string", + "confidence_cap": "string", + "blocked": false, + "missing_evidence": [], + "event_nodes": [], + "secondary_context": [], + "primary_drivers": [] + } +} +``` + +## Node Types + +- `judgement`: strict workflow verdict and score. +- `dasha_window`: Vimshottari or Narayana current window. +- `convergence`: domain convergence from local Dasha convergence. +- `external_window`: VedAstro adapter range-scan event evidence after provenance filtering. + +## Boundary + +- This is not `EventsAtRange` parity. +- It does not create new event predictions. +- It reflects already-collected strict workflow evidence. +- External windows remain oracle evidence until promoted by adjudicator tests. + +## Why This Matters + +The parity matrix identified Life Event Graphs as a P0 VedAstro gap. This v1 creates the stable semantic payload that the local PWA can render later without changing the adjudicator evidence contract. + +## Verification + +- `tests/test_life_event_graph_v1.py` +- `tests/test_mcp_strict_workflow_relationship.py` +- `tests/test_mcp_strict_workflow_career.py` +- `tests/test_mcp_strict_workflow_finance.py` diff --git a/mcp_server.py b/mcp_server.py index 4d50bc7b..a83f5588 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -1173,6 +1173,109 @@ def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[s } +def _build_life_event_graph(route: str, strict: Dict[str, Any]) -> Dict[str, Any]: + event_judgement = strict.get("event_judgement") if isinstance(strict, dict) else {} + present = strict.get("present_evidence") if isinstance(strict, dict) else {} + if not isinstance(event_judgement, dict): + event_judgement = {} + if not isinstance(present, dict): + present = {} + + nodes: List[Dict[str, Any]] = [] + nodes.append( + { + "kind": "judgement", + "label": event_judgement.get("dominant_label") or event_judgement.get("event_family") or route, + "verdict": event_judgement.get("verdict"), + "score": event_judgement.get("score"), + "source": "strict_workflow", + } + ) + + vim = present.get("vimshottari_current") + if isinstance(vim, dict): + md = vim.get("mahadasha") + ad = vim.get("antardasha") + label = "/".join([part for part in (md, ad) if part]) + if label: + nodes.append( + { + "kind": "dasha_window", + "label": label, + "source": "vimshottari_current", + } + ) + + narayana = present.get("narayana_current") + if isinstance(narayana, dict): + sign = narayana.get("sign") + lord = narayana.get("lord") + label = "/".join([part for part in (sign, lord) if part]) + if label: + nodes.append( + { + "kind": "dasha_window", + "label": label, + "source": "narayana_current", + } + ) + + convergence_keys = ( + "marriage_convergence", + "career_convergence", + "wealth_convergence", + "gains_convergence", + ) + for key in convergence_keys: + convergence = present.get(key) + if not isinstance(convergence, dict) or not convergence: + continue + nodes.append( + { + "kind": "convergence", + "label": key, + "level": convergence.get("convergence_level"), + "probability": convergence.get("probability"), + "source": "dasa_convergence", + } + ) + + external_activation = present.get("external_activation") + if isinstance(external_activation, dict): + for event in external_activation.get("events") or []: + if not isinstance(event, dict): + continue + nodes.append( + { + "kind": "external_window", + "label": event.get("event_id"), + "score": event.get("score"), + "start": event.get("start"), + "end": event.get("end"), + "tags": event.get("tags") or [], + "source": event.get("source") or external_activation.get("source"), + } + ) + + return { + "version": "life_event_graph_v1", + "route": route, + "dominant_label": event_judgement.get("dominant_label"), + "verdict": event_judgement.get("verdict"), + "confidence_cap": strict.get("confidence_cap"), + "blocked": bool(strict.get("blocked")), + "missing_evidence": strict.get("missing_evidence") or [], + "event_nodes": nodes, + "secondary_context": event_judgement.get("secondary_context") or [], + "primary_drivers": event_judgement.get("primary_drivers") or [], + } + + +def _with_life_event_graph(route: str, strict: Dict[str, Any]) -> Dict[str, Any]: + strict["life_event_graph"] = _build_life_event_graph(route, strict) + return strict + + def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, Any]: modules = result.get("modules", {}) if isinstance(result, dict) else {} domain_activations = _safe_get(modules, "dasa_convergence", "domain_activations") or {} @@ -1217,7 +1320,7 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An else: confidence_cap = "medium-low" event_judgement = _derive_event_judgement(route, present, missing) - return { + return _with_life_event_graph(route, { "question_type": route, "required_evidence": required, "present_evidence": present, @@ -1229,7 +1332,7 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An "Career timing requires D10 + A10/Karma Pada + AmK/Karakamsha " "plus dual dasha and career convergence support." ), - } + }) if route == "relationship": required = [ @@ -1279,7 +1382,7 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An else: confidence_cap = "medium-low" event_judgement = _derive_event_judgement(route, present, missing) - return { + return _with_life_event_graph(route, { "question_type": route, "required_evidence": required, "present_evidence": present, @@ -1291,7 +1394,7 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An "Marriage timing requires D9 + UL + DK + dual dasha + Vivah Saham " "and convergence support; missing links cap confidence." ), - } + }) if route == "finance": avayogi_risk = _check_external_avayogi_risk(result) @@ -1359,7 +1462,7 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An promise = present.get("wealth_promise_strength") or {} if "yogi" in promise.get("supporting_sources", []) and event_judgement.get("dominant_label") and "yogi_active" not in event_judgement.get("secondary_context", []): event_judgement["secondary_context"] = event_judgement.get("secondary_context", []) + ["yogi_active"] - return { + return _with_life_event_graph(route, { "question_type": route, "required_evidence": required, "present_evidence": present, @@ -1371,9 +1474,9 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An "Finance timing requires D2/D10 + strength + SAV + dual dasha " "plus at least one wealth-related convergence domain." ), - } + }) - return { + return _with_life_event_graph(route, { "question_type": route, "required_evidence": [], "present_evidence": {}, @@ -1382,7 +1485,7 @@ def _collect_strict_evidence(route: str, result: Dict[str, Any]) -> Dict[str, An "blocked": False, "event_judgement": _derive_event_judgement(route, {}, []), "reason": "Route-specific strict evidence audit is currently implemented for relationship and finance timing.", - } + }) # ============================================================================ @@ -1892,6 +1995,52 @@ def strict_workflow( return result +@mcp.tool() +def life_event_graph( + question: str, + year: int, + month: int, + day: int, + hour: int, + minute: int, + lat: float, + lon: float, + tz: float, + age: int, + transit_date: str, + node_mode: str = "mean", +) -> Dict[str, Any]: + """ + Build a graph-friendly event timeline from strict workflow evidence. + + This tool reuses the local full-reading pipeline plus strict adjudicator + evidence and optional VedAstro range-scan windows already present in the + evidence ledger. It does not claim external oracle closure by itself. + """ + result = strict_workflow( + question=question, + year=year, + month=month, + day=day, + hour=hour, + minute=minute, + lat=lat, + lon=lon, + tz=tz, + age=age, + transit_date=transit_date, + node_mode=node_mode, + ) + route = _safe_get(result, "routing", "question_type") or "general" + strict = result.get("strict_workflow") if isinstance(result, dict) else {} + return { + "question": question, + "route": route, + "life_event_graph": _build_life_event_graph(route, strict if isinstance(strict, dict) else {}), + "strict_workflow": strict, + } + + # ============================================================================ # Resources # ============================================================================ diff --git a/tests/test_life_event_graph_v1.py b/tests/test_life_event_graph_v1.py new file mode 100644 index 00000000..13aedcea --- /dev/null +++ b/tests/test_life_event_graph_v1.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Regression tests for Life Event Graph v1.""" + +from __future__ import annotations + +from mcp_server import _build_life_event_graph, _collect_strict_evidence + + +def test_life_event_graph_folds_strict_evidence_and_vedastro_top_event() -> None: + strict = { + "question_type": "relationship", + "event_judgement": { + "event_family": "relationship", + "score": 85, + "verdict": "high_probability_window", + "dominant_label": "legal_marriage", + "secondary_context": [ + "darakaraka_active", + "jaimini_support", + "ul_support", + "external_activation_support", + "synastry_support", + ], + "primary_drivers": [ + "marriage_convergence", + "vimshottari_current", + "narayana_current", + "darakaraka", + "upapada_lagna", + ], + }, + "present_evidence": { + "vimshottari_current": {"mahadasha": "Venus", "antardasha": "Moon"}, + "narayana_current": {"sign": "Libra", "lord": "Venus"}, + "marriage_convergence": {"convergence_level": "L4", "probability": "70-85%"}, + "external_activation": { + "level": "moderate", + "source": "vedastro_service_adapter_candidate", + "signals": ["vedastro_range_scan"], + "events": [ + { + "event_id": "jupiter_7h_window", + "score": 72, + "start": "2026-05-01", + "end": "2026-06-01", + "tags": ["marriage", "transit"], + } + ], + }, + }, + "confidence_cap": "medium-high", + "missing_evidence": [], + "blocked": False, + } + + graph = _build_life_event_graph("relationship", strict) + + assert graph["version"] == "life_event_graph_v1" + assert graph["route"] == "relationship" + assert graph["dominant_label"] == "legal_marriage" + assert graph["confidence_cap"] == "medium-high" + assert graph["blocked"] is False + assert graph["event_nodes"][0] == { + "kind": "judgement", + "label": "legal_marriage", + "verdict": "high_probability_window", + "score": 85, + "source": "strict_workflow", + } + assert graph["event_nodes"][1] == { + "kind": "dasha_window", + "label": "Venus/Moon", + "source": "vimshottari_current", + } + assert graph["event_nodes"][2] == { + "kind": "dasha_window", + "label": "Libra/Venus", + "source": "narayana_current", + } + assert graph["event_nodes"][3] == { + "kind": "convergence", + "label": "marriage_convergence", + "level": "L4", + "probability": "70-85%", + "source": "dasa_convergence", + } + assert graph["event_nodes"][4] == { + "kind": "external_window", + "label": "jupiter_7h_window", + "score": 72, + "start": "2026-05-01", + "end": "2026-06-01", + "tags": ["marriage", "transit"], + "source": "vedastro_service_adapter_candidate", + } + + +def test_life_event_graph_is_returned_from_strict_relationship_evidence() -> None: + result = { + "modules": { + "varga_full": {"D9_Navamsa": {"summary": "ok"}}, + "special_lagnas": {"Upapada_Lagna": {"sign": "Libra", "lord": "Venus"}}, + "jaimini": { + "darakaraka": {"planet": "Venus", "house": 7}, + "marriage_support": {"dk_7h_link": True}, + }, + "vivah_saham": {"sign": "Taurus", "house": 7}, + "dasha": {"current_dasha": {"mahadasha": "Venus", "antardasha": "Moon"}}, + "narayana_dasha": {"current_dasha": {"sign": "Libra", "lord": "Venus"}}, + "dasa_convergence": { + "domain_activations": { + "marriage_partnership": {"convergence_level": "L4", "probability": "70-85%"} + } + }, + "external_activation": { + "evidence_ledger": [ + { + "source": "vedastro_service_adapter_candidate", + "operation": "range_scan", + "domain": "marriage", + "event_id": "jupiter_7h_window", + "score": 72, + "start": "2026-05-01", + "end": "2026-06-01", + "tags": ["marriage", "transit"], + } + ] + }, + } + } + + strict = _collect_strict_evidence("relationship", result) + + assert strict["life_event_graph"]["version"] == "life_event_graph_v1" + assert strict["life_event_graph"]["route"] == "relationship" + assert strict["life_event_graph"]["dominant_label"] == "legal_marriage" + assert any(node["kind"] == "external_window" for node in strict["life_event_graph"]["event_nodes"])