734d20590c
Consultation and personal reports otherwise ignore the merged friendship table, timing seed, module audit, and Pratyantar windows. Co-authored-by: Cursor <cursoragent@cursor.com>
288 lines
12 KiB
Python
288 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Attach merged-engine fields to the commercial consultation path.
|
|
|
|
Task 6 of TASK-upstream-sync-20260903: the three-way merge left
|
|
planetary_friendship, timing_narrative, module_execution_audit, and
|
|
pratyantar_dasha_timeline on the engine. The web consultation workflow does
|
|
not call cmd_full_reading, so those keys never reached /api/consultation_workflow
|
|
until this module copies a compact, allowlisted subset onto the chart and
|
|
response. Import-missing research helpers stay blocked. No nadi/classical
|
|
loci, no birth-data files.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
_HEADLINE_MAX = 300
|
|
_LINE_MAX = 400
|
|
_LIST_MAX = 8
|
|
ASHTOTTARI_PARTIAL_NOTE = "参数敏感、未验证"
|
|
|
|
|
|
def _clip_text(value: Any, limit: int) -> str:
|
|
text = " ".join(str(value or "").split())
|
|
if len(text) <= limit:
|
|
return text
|
|
return text[: max(0, limit - 1)].rstrip() + "…"
|
|
|
|
|
|
def _clip_lines(values: Any) -> list[str]:
|
|
if not isinstance(values, list):
|
|
return []
|
|
out: list[str] = []
|
|
for item in values:
|
|
text = _clip_text(item, _LINE_MAX)
|
|
if text:
|
|
out.append(text)
|
|
if len(out) >= _LIST_MAX:
|
|
break
|
|
return out
|
|
|
|
|
|
def _load_engine():
|
|
scripts_dir = str(Path(__file__).resolve().parent)
|
|
if scripts_dir not in sys.path:
|
|
sys.path.insert(0, scripts_dir)
|
|
try:
|
|
from scripts import jyotish_engine as engine
|
|
except ModuleNotFoundError: # pragma: no cover - script execution
|
|
import jyotish_engine as engine
|
|
return engine
|
|
|
|
|
|
def _modules_for_timing(modules: dict | None) -> dict[str, Any]:
|
|
view = dict(modules) if isinstance(modules, dict) else {}
|
|
dasha = dict(view.get("dasha") or {}) if isinstance(view.get("dasha"), dict) else {}
|
|
current = dasha.get("current_dasha") if isinstance(dasha.get("current_dasha"), dict) else {}
|
|
if not current.get("mahadasha"):
|
|
sub = view.get("dasha_sub_periods") if isinstance(view.get("dasha_sub_periods"), dict) else {}
|
|
running = sub.get("current") if isinstance(sub.get("current"), dict) else {}
|
|
maha = running.get("mahadasha") if isinstance(running.get("mahadasha"), dict) else {}
|
|
lord = maha.get("lord")
|
|
if lord:
|
|
dasha["current_dasha"] = {"mahadasha": lord}
|
|
view["dasha"] = dasha
|
|
return view
|
|
|
|
|
|
def compact_timing_narrative(modules: dict | None) -> dict[str, Any]:
|
|
"""Length-capped copy of the engine timing seed for the chat prompt pack."""
|
|
try:
|
|
engine = _load_engine()
|
|
payload = engine._build_timing_narrative_payload(_modules_for_timing(modules))
|
|
except Exception as exc:
|
|
return {"status": "blocked", "reason": f"timing_narrative_unavailable:{exc.__class__.__name__}"}
|
|
if not isinstance(payload, dict):
|
|
return {"status": "blocked", "reason": "timing_narrative_unavailable"}
|
|
return {
|
|
"status": payload.get("status") or "parameter_sensitive",
|
|
"headline": _clip_text(payload.get("headline"), _HEADLINE_MAX),
|
|
"strengths": _clip_lines(payload.get("strengths")),
|
|
"risks": _clip_lines(payload.get("risks")),
|
|
"boundaries": _clip_lines(payload.get("boundaries")),
|
|
}
|
|
|
|
|
|
def compact_planetary_friendship(snapshot: dict | None) -> dict[str, Any]:
|
|
if not isinstance(snapshot, dict):
|
|
return {"status": "blocked", "reason": "planetary_friendship_unavailable", "rows": []}
|
|
rows = []
|
|
for raw in snapshot.get("rows") or []:
|
|
if not isinstance(raw, dict) or not raw.get("planet"):
|
|
continue
|
|
compound = raw.get("compound") if isinstance(raw.get("compound"), dict) else {}
|
|
|
|
def names(key: str) -> list[str]:
|
|
values = compound.get(key)
|
|
if not isinstance(values, list):
|
|
return []
|
|
return [str(item) for item in values if item][:8]
|
|
|
|
rows.append({
|
|
"planet": str(raw["planet"]),
|
|
"great_friends": names("great_friends"),
|
|
"friends": names("friends"),
|
|
"neutral": names("neutral"),
|
|
"enemies": names("enemies"),
|
|
"great_enemies": names("great_enemies"),
|
|
})
|
|
if len(rows) >= 9:
|
|
break
|
|
return {
|
|
"status": snapshot.get("status") or "parameter_sensitive",
|
|
"rows": rows,
|
|
"boundary": str(snapshot.get("boundary") or "local raw relationship snapshot only"),
|
|
}
|
|
|
|
|
|
def compact_pratyantar_timeline(sub_periods: dict | None, reference_dt: datetime) -> dict[str, Any]:
|
|
"""Current and next Pratyantar boundaries for the running antardasha."""
|
|
if not isinstance(sub_periods, dict):
|
|
return {"status": "blocked", "reason": "dasha_sub_periods_missing"}
|
|
current = sub_periods.get("current") if isinstance(sub_periods.get("current"), dict) else {}
|
|
maha = current.get("mahadasha") if isinstance(current.get("mahadasha"), dict) else {}
|
|
antar = current.get("antardasha") if isinstance(current.get("antardasha"), dict) else {}
|
|
md_lord = str(maha.get("lord") or "")
|
|
ad_lord = str(antar.get("lord") or "")
|
|
start = _parse_date(antar.get("start"))
|
|
end = _parse_date(antar.get("end"))
|
|
if not md_lord or not ad_lord or start is None or end is None or end <= start:
|
|
return {"status": "blocked", "reason": "antardasha_boundaries_unavailable"}
|
|
engine = _load_engine()
|
|
try:
|
|
timeline = engine._build_pratyantar_timeline(md_lord, ad_lord, start, end, reference_dt=reference_dt)
|
|
except Exception as exc:
|
|
return {"status": "blocked", "reason": f"pratyantar_unavailable:{exc.__class__.__name__}"}
|
|
if not isinstance(timeline, list) or not timeline:
|
|
return {"status": "blocked", "reason": "pratyantar_timeline_empty"}
|
|
current_row = next((row for row in timeline if isinstance(row, dict) and row.get("is_current")), None)
|
|
if current_row is None:
|
|
current_row = timeline[0]
|
|
next_row = None
|
|
if isinstance(current_row, dict):
|
|
try:
|
|
index = timeline.index(current_row)
|
|
next_row = timeline[index + 1] if index + 1 < len(timeline) else None
|
|
except ValueError:
|
|
next_row = None
|
|
|
|
def boundary(row: dict | None) -> dict[str, str] | None:
|
|
if not isinstance(row, dict) or not row.get("lord"):
|
|
return None
|
|
return {
|
|
"lord": str(row["lord"]),
|
|
"start": str(row.get("start") or ""),
|
|
"end": str(row.get("end") or ""),
|
|
}
|
|
|
|
current_boundary = boundary(current_row)
|
|
next_boundary = boundary(next_row if isinstance(next_row, dict) else None)
|
|
return {
|
|
"status": "ready",
|
|
"current": current_boundary,
|
|
"next": next_boundary,
|
|
"boundary": "candidate window inside the running antardasha; not a verified event date",
|
|
}
|
|
|
|
|
|
def module_execution_audit_for_chart(chart: dict | None) -> list[dict[str, Any]]:
|
|
engine = _load_engine()
|
|
modules = (chart or {}).get("modules") if isinstance((chart or {}).get("modules"), dict) else {}
|
|
packet = {
|
|
"raw_full_reading": {"modules": modules},
|
|
"calculation_profile_id": "consultation_workflow_local",
|
|
"raw_module_usage_map": {},
|
|
}
|
|
try:
|
|
rows = engine._build_module_execution_audit(packet)
|
|
except Exception:
|
|
return []
|
|
if not isinstance(rows, list):
|
|
return []
|
|
compact = []
|
|
for row in rows:
|
|
if not isinstance(row, dict) or not row.get("module_id"):
|
|
continue
|
|
if str(row["module_id"]) == "module_execution_audit":
|
|
continue
|
|
compact.append({
|
|
"module_id": str(row["module_id"]),
|
|
"status": str(row.get("status") or "available"),
|
|
"conclusion_use": str(row.get("conclusion_use") or "raw_appendix_only"),
|
|
"limitation": _clip_text(row.get("limitation"), _LINE_MAX),
|
|
})
|
|
if len(compact) >= 40:
|
|
break
|
|
return compact
|
|
|
|
|
|
def extra_technique_audit_rows(chart: dict | None) -> list[dict[str, Any]]:
|
|
"""Audit rows derived from the merged fields. Partial never reads as certainty."""
|
|
modules = (chart or {}).get("modules") if isinstance((chart or {}).get("modules"), dict) else {}
|
|
friendship = modules.get("planetary_friendship") if isinstance(modules.get("planetary_friendship"), dict) else {}
|
|
friendship_rows = friendship.get("rows") if isinstance(friendship.get("rows"), list) else []
|
|
friendship_status = "executed" if friendship_rows else "blocked"
|
|
audit = module_execution_audit_for_chart(chart)
|
|
return [
|
|
{
|
|
"technique": "Planetary Friendship",
|
|
"status": friendship_status,
|
|
"system": "jyotish",
|
|
"boundary": "友敌等级表;不得当作确定性结论",
|
|
},
|
|
{
|
|
"technique": "Ashtottari Dasha",
|
|
"status": "partial",
|
|
"system": "jyotish",
|
|
"boundary": ASHTOTTARI_PARTIAL_NOTE,
|
|
},
|
|
{
|
|
"technique": "Module Execution Audit",
|
|
"status": "executed" if audit else "blocked",
|
|
"system": "jyotish",
|
|
"boundary": "附录可见;不得升格为确定性结论",
|
|
},
|
|
]
|
|
|
|
|
|
def attach_merged_engine_fields(chart: dict, *, reference_dt: datetime) -> dict:
|
|
"""Mutate chart.modules with friendship + pratyantar. Safe on empty charts."""
|
|
if not isinstance(chart, dict):
|
|
return chart
|
|
modules = chart.get("modules") if isinstance(chart.get("modules"), dict) else {}
|
|
chart["modules"] = modules
|
|
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
|
|
if planets and not isinstance(modules.get("planetary_friendship"), dict):
|
|
try:
|
|
engine = _load_engine()
|
|
snapshot = engine._build_planetary_friendship_snapshot(planets)
|
|
modules["planetary_friendship"] = compact_planetary_friendship(snapshot)
|
|
except Exception as exc:
|
|
modules["planetary_friendship"] = {
|
|
"status": "blocked",
|
|
"reason": f"planetary_friendship_unavailable:{exc.__class__.__name__}",
|
|
"rows": [],
|
|
}
|
|
sub_periods = modules.get("dasha_sub_periods") if isinstance(modules.get("dasha_sub_periods"), dict) else None
|
|
pratyantar = compact_pratyantar_timeline(sub_periods, reference_dt)
|
|
if isinstance(sub_periods, dict):
|
|
sub_periods["pratyantar_dasha_timeline"] = pratyantar
|
|
modules["pratyantar_dasha_timeline"] = pratyantar
|
|
modules["ashtottari_dasha"] = {
|
|
"status": "parameter_sensitive",
|
|
"overlay_status": "partial",
|
|
"claim_boundary": ASHTOTTARI_PARTIAL_NOTE,
|
|
"execution_status": "not_asserted_as_deterministic",
|
|
}
|
|
modules["module_execution_audit"] = module_execution_audit_for_chart(chart)
|
|
return chart
|
|
|
|
|
|
def consultation_workflow_extra_fields(chart: dict | None) -> dict[str, Any]:
|
|
modules = (chart or {}).get("modules") if isinstance((chart or {}).get("modules"), dict) else {}
|
|
snapshot = (((chart or {}).get("ai_prompt_pack") or {}).get("evidence_snapshot") or {}) if isinstance(chart, dict) else {}
|
|
timing = snapshot.get("timing_narrative") if isinstance(snapshot.get("timing_narrative"), dict) else compact_timing_narrative(modules)
|
|
return {
|
|
"timing_narrative": timing,
|
|
"module_execution_audit": modules.get("module_execution_audit") or module_execution_audit_for_chart(chart),
|
|
"pratyantar_dasha_timeline": modules.get("pratyantar_dasha_timeline") or {"status": "blocked"},
|
|
}
|
|
|
|
|
|
def _parse_date(value: Any) -> datetime | None:
|
|
if isinstance(value, datetime):
|
|
return value.replace(tzinfo=None)
|
|
if isinstance(value, str) and value.strip():
|
|
text = value.strip().replace("Z", "+00:00")
|
|
try:
|
|
if "T" in text:
|
|
return datetime.fromisoformat(text).replace(tzinfo=None)
|
|
return datetime.strptime(text[:10], "%Y-%m-%d")
|
|
except ValueError:
|
|
return None
|
|
return None
|