Add life event graph v1 to strict workflows

This commit is contained in:
732642856
2026-06-28 16:42:33 +08:00
parent e869f561ff
commit c0f78bd5e4
4 changed files with 346 additions and 8 deletions
+1
View File
@@ -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.
@@ -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`
+157 -8
View File
@@ -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
# ============================================================================
+137
View File
@@ -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"])