Add life event graph v1 to strict workflows
This commit is contained in:
@@ -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
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -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"])
|
||||
Reference in New Issue
Block a user