Harden high-rigor Jyotish timing workflow
This commit is contained in:
@@ -60,6 +60,7 @@ API_COMMAND_MAP = {
|
||||
"transit-trigger": "/api/transit",
|
||||
"audit-capabilities": "/api/capability_audit",
|
||||
"thematic-report": "/api/thematic_report",
|
||||
"high-rigor-workflow": "/api/high_rigor_workflow",
|
||||
"report-artifact": "/api/report_artifact",
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,10 @@ def cmd_narayana_dasha(args, chart_data):
|
||||
if ad:
|
||||
lines.append(f" Antardasha: {ad['sign']}({ad['lord']}){ad['years']}年")
|
||||
|
||||
pd = curr.get('pd')
|
||||
if pd:
|
||||
lines.append(f" Pratyantardasha: {pd['sign']}({pd['lord']}){pd['years']}年")
|
||||
|
||||
lines.append("")
|
||||
for line in result.get('interpretation', []):
|
||||
lines.append(f" {line}")
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Report whether the current runtime is fast fallback or VedAstro official mode."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from scripts.local_env import load_local_env
|
||||
except ModuleNotFoundError: # pragma: no cover - direct script execution
|
||||
from local_env import load_local_env
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FAST_TIMEOUT_THRESHOLD_SECONDS = 5.0
|
||||
|
||||
|
||||
def _bool_env(name: str) -> bool:
|
||||
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _timeout_seconds() -> float:
|
||||
raw = os.environ.get("VEDASTRO_TIMEOUT_SECONDS", "").strip()
|
||||
if not raw:
|
||||
return 4.0
|
||||
try:
|
||||
return float(raw)
|
||||
except ValueError:
|
||||
return 4.0
|
||||
|
||||
|
||||
def build_report() -> dict:
|
||||
load_local_env(ROOT)
|
||||
endpoint = os.environ.get("VEDASTRO_API_ENDPOINT", "").strip()
|
||||
network_enabled = _bool_env("VEDASTRO_ENABLE_NETWORK")
|
||||
timeout_seconds = _timeout_seconds()
|
||||
has_api_key = bool(os.environ.get("VEDASTRO_API_KEY", "").strip())
|
||||
missing = []
|
||||
if not endpoint:
|
||||
missing.append("VEDASTRO_API_ENDPOINT")
|
||||
if not network_enabled:
|
||||
missing.append("VEDASTRO_ENABLE_NETWORK=1")
|
||||
if timeout_seconds <= FAST_TIMEOUT_THRESHOLD_SECONDS:
|
||||
missing.append("VEDASTRO_TIMEOUT_SECONDS>5")
|
||||
official_ready = not missing
|
||||
mode = "official_extended" if official_ready else "fast_local_fallback"
|
||||
return {
|
||||
"mode": mode,
|
||||
"official_ready": official_ready,
|
||||
"endpoint_configured": bool(endpoint),
|
||||
"network_enabled": network_enabled,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"has_api_key": has_api_key,
|
||||
"missing": missing,
|
||||
"expected_fallback_status": (
|
||||
"none_if_official_endpoint_responds"
|
||||
if official_ready
|
||||
else "official_snapshot_budget_exhausted_or_endpoint_blocked"
|
||||
),
|
||||
"next_step": (
|
||||
"Run full-reading or strict_workflow; verify vedastro_official.status is ok/partial."
|
||||
if official_ready
|
||||
else "Copy .env.official.example to .env.local and fill endpoint/network settings for official mode."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
|
||||
args = parser.parse_args()
|
||||
report = build_report()
|
||||
if args.json:
|
||||
print(json.dumps(report, ensure_ascii=False, sort_keys=True))
|
||||
return 0
|
||||
print(f"VedAstro runtime mode: {report['mode']}")
|
||||
print(f"official_ready: {str(report['official_ready']).lower()}")
|
||||
print(f"endpoint_configured: {str(report['endpoint_configured']).lower()}")
|
||||
print(f"network_enabled: {str(report['network_enabled']).lower()}")
|
||||
print(f"timeout_seconds: {report['timeout_seconds']}")
|
||||
print(f"has_api_key: {str(report['has_api_key']).lower()}")
|
||||
if report["missing"]:
|
||||
print("missing:")
|
||||
for item in report["missing"]:
|
||||
print(f" - {item}")
|
||||
print(f"expected_fallback_status: {report['expected_fallback_status']}")
|
||||
print(f"next_step: {report['next_step']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -70,6 +70,49 @@ def _evidence_line(label: str, value: Any) -> dict[str, str]:
|
||||
return {"label": label, "value": str(value)}
|
||||
|
||||
|
||||
def _strict_contracts(report: dict[str, Any], modules: dict[str, Any]) -> dict[str, Any]:
|
||||
snapshot = _as_dict(_as_dict(_as_dict(report.get("ai_prompt_pack")).get("evidence_snapshot")))
|
||||
contracts = _as_dict(snapshot.get("strict_workflow_contracts"))
|
||||
if contracts:
|
||||
return contracts
|
||||
mapping = {
|
||||
"career": "career_strict_evidence",
|
||||
"relationship": "relationship_strict_evidence",
|
||||
"finance": "finance_strict_evidence",
|
||||
}
|
||||
compact: dict[str, Any] = {}
|
||||
for route, key in mapping.items():
|
||||
strict = _as_dict(modules.get(key))
|
||||
if strict:
|
||||
compact[route] = strict
|
||||
return compact
|
||||
|
||||
|
||||
def _topic_audit_gate(contracts: dict[str, Any], route: str) -> dict[str, Any]:
|
||||
contract = _as_dict(contracts.get(route))
|
||||
bundle = _as_dict(contract.get("strict_adjudication_bundle"))
|
||||
summary = _as_dict(bundle.get("strict_audit_gate")) or _as_dict(contract.get("technique_audit_summary"))
|
||||
if summary:
|
||||
return summary
|
||||
return {
|
||||
"functional_benefic_malefic": {"gate": "hard", "used": False, "status": "blocked"},
|
||||
"relevant_vargas": {"gate": "hard", "required_keys": [], "present_keys": []},
|
||||
"vimshottari_narayana_crosscheck": {
|
||||
"gate": "hard",
|
||||
"used": False,
|
||||
"required_timing_systems": ["Vimshottari", "Narayana"],
|
||||
},
|
||||
"source_priority_boundary": {
|
||||
"gate": "boundary",
|
||||
"official": {},
|
||||
"local": {},
|
||||
"fallback_used": [],
|
||||
"blocked_items": [],
|
||||
"conflicts": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _topic(
|
||||
*,
|
||||
topic_id: str,
|
||||
@@ -79,6 +122,9 @@ def _topic(
|
||||
evidence: list[dict[str, str]],
|
||||
confidence: str,
|
||||
vedastro: dict[str, Any],
|
||||
strict_audit_gate: dict[str, Any],
|
||||
monthly_adjudication_summary: dict[str, Any],
|
||||
official_day_signal_summary: dict[str, Any],
|
||||
questions: list[str],
|
||||
answer_mode: str = "tap_or_ask",
|
||||
priority: int = 50,
|
||||
@@ -91,14 +137,62 @@ def _topic(
|
||||
"evidence": evidence,
|
||||
"confidence": confidence,
|
||||
"vedastro": vedastro,
|
||||
"strict_adjudication_bundle": {
|
||||
"strict_audit_gate": strict_audit_gate,
|
||||
"monthly_adjudication_summary": monthly_adjudication_summary,
|
||||
"official_day_signal_summary": official_day_signal_summary,
|
||||
},
|
||||
"strict_audit_gate": strict_audit_gate,
|
||||
"monthly_adjudication_summary": monthly_adjudication_summary,
|
||||
"official_day_signal_summary": official_day_signal_summary,
|
||||
"suggested_questions": questions,
|
||||
"answer_mode": answer_mode,
|
||||
"priority": priority,
|
||||
}
|
||||
|
||||
|
||||
def _topic_official_day_signal_summary(contracts: dict[str, Any], modules: dict[str, Any], route: str) -> dict[str, Any]:
|
||||
contract = _as_dict(contracts.get(route))
|
||||
bundle = _as_dict(contract.get("strict_adjudication_bundle"))
|
||||
summary = _as_dict(bundle.get("official_day_signal_summary")) or _as_dict(contract.get("official_day_signal_summary"))
|
||||
if summary:
|
||||
return summary
|
||||
mapping = {
|
||||
"career": "career_strict_evidence",
|
||||
"relationship": "relationship_strict_evidence",
|
||||
"finance": "finance_strict_evidence",
|
||||
}
|
||||
strict = _as_dict(modules.get(mapping.get(route, "")))
|
||||
present = _as_dict(strict.get("present_evidence"))
|
||||
external = _as_dict(present.get("external_activation"))
|
||||
signals = _as_list(external.get("official_day_signals"))
|
||||
return {
|
||||
"available": bool(signals),
|
||||
"signal_count": len(signals),
|
||||
"top_day": _as_dict(signals[0]) if signals else None,
|
||||
"days": [_as_dict(item) for item in signals[:3] if isinstance(item, dict)],
|
||||
"source": "present_evidence.external_activation.official_day_signals" if signals else None,
|
||||
}
|
||||
|
||||
|
||||
def _topic_monthly_adjudication_summary(contracts: dict[str, Any], modules: dict[str, Any], route: str) -> dict[str, Any]:
|
||||
contract = _as_dict(contracts.get(route))
|
||||
bundle = _as_dict(contract.get("strict_adjudication_bundle"))
|
||||
summary = _as_dict(bundle.get("monthly_adjudication_summary")) or _as_dict(contract.get("monthly_adjudication_summary"))
|
||||
if summary:
|
||||
return summary
|
||||
mapping = {
|
||||
"career": "career_strict_evidence",
|
||||
"relationship": "relationship_strict_evidence",
|
||||
"finance": "finance_strict_evidence",
|
||||
}
|
||||
strict = _as_dict(modules.get(mapping.get(route, "")))
|
||||
return _as_dict(strict.get("monthly_adjudication_summary"))
|
||||
|
||||
|
||||
def build_guided_topics(report: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
modules = _as_dict(report.get("modules"))
|
||||
contracts = _strict_contracts(report, modules)
|
||||
chart = _as_dict(report.get("chart") or modules.get("chart"))
|
||||
planets = _as_dict(chart.get("planets"))
|
||||
md, ad, md_start, md_end = _current_dasha(modules)
|
||||
@@ -133,6 +227,9 @@ def build_guided_topics(report: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
],
|
||||
confidence="medium" if marriage_conv or relationship else "low",
|
||||
vedastro=_vedastro_snapshot(modules, "marriage"),
|
||||
strict_audit_gate=_topic_audit_gate(contracts, "relationship"),
|
||||
monthly_adjudication_summary=_topic_monthly_adjudication_summary(contracts, modules, "relationship"),
|
||||
official_day_signal_summary=_topic_official_day_signal_summary(contracts, modules, "relationship"),
|
||||
questions=[
|
||||
"我现在适合认真发展关系,还是更适合筛选和观察?",
|
||||
"我的伴侣画像、认识场景和相处风险是什么?",
|
||||
@@ -153,6 +250,9 @@ def build_guided_topics(report: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
],
|
||||
confidence="medium" if career_conv or ketu_house == 10 else "low",
|
||||
vedastro=_vedastro_snapshot(modules, "career"),
|
||||
strict_audit_gate=_topic_audit_gate(contracts, "career"),
|
||||
monthly_adjudication_summary=_topic_monthly_adjudication_summary(contracts, modules, "career"),
|
||||
official_day_signal_summary=_topic_official_day_signal_summary(contracts, modules, "career"),
|
||||
questions=[
|
||||
"我现在适合换方向还是继续深耕?",
|
||||
"2026 年事业吉利在哪里,不利在哪里?",
|
||||
@@ -172,6 +272,9 @@ def build_guided_topics(report: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
],
|
||||
confidence="medium",
|
||||
vedastro=_vedastro_snapshot(modules, "marriage"),
|
||||
strict_audit_gate=_topic_audit_gate(contracts, "relationship"),
|
||||
monthly_adjudication_summary=_topic_monthly_adjudication_summary(contracts, modules, "relationship"),
|
||||
official_day_signal_summary=_topic_official_day_signal_summary(contracts, modules, "relationship"),
|
||||
questions=[
|
||||
"我可以用过去事件校正出生时间吗?",
|
||||
"哪些人生事件最适合用来校正出生时间?",
|
||||
@@ -192,6 +295,9 @@ def build_guided_topics(report: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
],
|
||||
confidence="medium" if wealth_conv else "low",
|
||||
vedastro=_vedastro_snapshot(modules, "wealth"),
|
||||
strict_audit_gate=_topic_audit_gate(contracts, "finance"),
|
||||
monthly_adjudication_summary=_topic_monthly_adjudication_summary(contracts, modules, "finance"),
|
||||
official_day_signal_summary=_topic_official_day_signal_summary(contracts, modules, "finance"),
|
||||
questions=[
|
||||
"2026 年哪些钱可以赚,哪些钱要避险?",
|
||||
"我适合靠项目、投资、合作还是长期积累赚钱?",
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reusable historical event backtest entrypoint built on strict workflow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import mcp_server
|
||||
|
||||
|
||||
SUPPORTED_DOMAINS = {
|
||||
"career": {
|
||||
"route": "career",
|
||||
"question": "请严格回测这条事业事件是否成立,并判断是职业状态、角色变化、升迁窗口还是项目兑现。",
|
||||
},
|
||||
"wealth": {
|
||||
"route": "finance",
|
||||
"question": "请严格回测这条财富事件是否成立,并判断更接近收入增长、到账、套现还是公众财富状态。",
|
||||
},
|
||||
"finance": {
|
||||
"route": "finance",
|
||||
"question": "请严格回测这条财富事件是否成立,并判断更接近收入增长、到账、套现还是公众财富状态。",
|
||||
},
|
||||
"marriage": {
|
||||
"route": "relationship",
|
||||
"question": "请严格回测这条婚恋事件是否成立,并判断是否达到正式关系或婚姻层。",
|
||||
},
|
||||
"relationship": {
|
||||
"route": "relationship",
|
||||
"question": "请严格回测这条婚恋事件是否成立,并判断是否达到正式关系或婚姻层。",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _route_for_domain(domain: str) -> dict[str, str] | None:
|
||||
return SUPPORTED_DOMAINS.get(str(domain).strip().lower())
|
||||
|
||||
|
||||
def _load_payload(path: str) -> dict[str, Any]:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def _event_result_class(
|
||||
verdict: str | None,
|
||||
blocked: bool,
|
||||
expected_label: str | None,
|
||||
actual_label: str | None,
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
if blocked:
|
||||
return "blocked", {"reason": "strict_workflow_blocked"}
|
||||
if verdict == "high_probability_window" and actual_label and (
|
||||
not expected_label or expected_label == actual_label
|
||||
):
|
||||
return "strong_hit", {"reason": "supported_route_and_label"}
|
||||
if verdict in {"high_probability_window", "moderate_probability_window"}:
|
||||
if expected_label and actual_label and expected_label != actual_label:
|
||||
return "weak_hit", {"reason": "label_mismatch_under_supported_route"}
|
||||
return "weak_hit", {"reason": "supported_route_without_exact_label"}
|
||||
if verdict == "weak_window_needs_confirmation":
|
||||
return "weak_hit", {"reason": "weak_window_needs_confirmation"}
|
||||
return "miss", {"reason": verdict or "insufficient_evidence"}
|
||||
|
||||
|
||||
def _official_snapshot_summary(strict: dict[str, Any]) -> dict[str, Any]:
|
||||
present = strict.get("present_evidence") or {}
|
||||
official = present.get("vedastro_official_snapshot")
|
||||
if not isinstance(official, dict):
|
||||
return {"level": "missing", "status": "missing", "source": None}
|
||||
return {
|
||||
"level": official.get("level") or "missing",
|
||||
"status": official.get("status"),
|
||||
"source": official.get("source"),
|
||||
}
|
||||
|
||||
|
||||
def _source_priority_mode(strict: dict[str, Any]) -> str | None:
|
||||
present = strict.get("present_evidence") or {}
|
||||
source_priority = present.get("source_priority")
|
||||
if not isinstance(source_priority, dict):
|
||||
return None
|
||||
return source_priority.get("mode")
|
||||
|
||||
|
||||
def _run_supported_event(subject: dict[str, Any], event: dict[str, Any]) -> dict[str, Any]:
|
||||
route_info = _route_for_domain(event.get("domain", ""))
|
||||
if route_info is None:
|
||||
return {
|
||||
"id": event.get("id"),
|
||||
"date": event.get("date"),
|
||||
"domain": event.get("domain"),
|
||||
"route": None,
|
||||
"expected_label": event.get("expected_label"),
|
||||
"actual_label": None,
|
||||
"matched_expected_label": False,
|
||||
"result_class": "unsupported_domain",
|
||||
"boundary": {"reason": "route_not_yet_implemented_for_event_backtest"},
|
||||
"official_snapshot": {"level": "missing", "status": "missing", "source": None},
|
||||
"evidence": {"source_priority_mode": None, "confidence_cap": "unsupported"},
|
||||
}
|
||||
|
||||
result = mcp_server.strict_workflow(
|
||||
question=route_info["question"],
|
||||
year=int(subject["year"]),
|
||||
month=int(subject["month"]),
|
||||
day=int(subject["day"]),
|
||||
hour=int(subject["hour"]),
|
||||
minute=int(subject["minute"]),
|
||||
lat=float(subject["lat"]),
|
||||
lon=float(subject["lon"]),
|
||||
tz=float(subject["tz"]),
|
||||
age=int(subject.get("age", 0)),
|
||||
transit_date=str(event["date"]),
|
||||
node_mode=str(subject.get("node_mode", "mean")),
|
||||
)
|
||||
strict = result.get("strict_workflow") if isinstance(result, dict) else {}
|
||||
if not isinstance(strict, dict):
|
||||
strict = {}
|
||||
|
||||
judgement = strict.get("event_judgement") if isinstance(strict.get("event_judgement"), dict) else {}
|
||||
actual_label = judgement.get("dominant_label")
|
||||
expected_label = event.get("expected_label")
|
||||
verdict = judgement.get("verdict")
|
||||
blocked = bool(strict.get("blocked"))
|
||||
result_class, boundary = _event_result_class(verdict, blocked, expected_label, actual_label)
|
||||
|
||||
return {
|
||||
"id": event.get("id"),
|
||||
"date": event.get("date"),
|
||||
"domain": event.get("domain"),
|
||||
"route": route_info["route"],
|
||||
"expected_label": expected_label,
|
||||
"actual_label": actual_label,
|
||||
"matched_expected_label": bool(expected_label and expected_label == actual_label),
|
||||
"result_class": result_class,
|
||||
"boundary": boundary,
|
||||
"official_snapshot": _official_snapshot_summary(strict),
|
||||
"evidence": {
|
||||
"verdict": verdict,
|
||||
"score": judgement.get("score"),
|
||||
"confidence_cap": strict.get("confidence_cap"),
|
||||
"missing_evidence": strict.get("missing_evidence") or [],
|
||||
"blocked_items": strict.get("blocked_items") or [],
|
||||
"conflicts": strict.get("conflicts") or [],
|
||||
"adjudication_stages": strict.get("adjudication_stages") or {},
|
||||
"multi_reference_reading_summary": strict.get("multi_reference_reading_summary") or {},
|
||||
"main_conflicts": strict.get("main_conflicts") or strict.get("conflicts") or [],
|
||||
"source_priority_mode": _source_priority_mode(strict),
|
||||
"primary_drivers": judgement.get("primary_drivers") or [],
|
||||
"secondary_context": judgement.get("secondary_context") or [],
|
||||
"technique_audit": strict.get("technique_audit") or [],
|
||||
"life_event_graph": strict.get("life_event_graph") or {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_report(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
subject = payload.get("subject") or {}
|
||||
events = payload.get("events") or []
|
||||
rows = [_run_supported_event(subject, event) for event in events]
|
||||
|
||||
summary = {
|
||||
"total_events": len(rows),
|
||||
"strong_hits": sum(1 for row in rows if row["result_class"] == "strong_hit"),
|
||||
"weak_hits": sum(1 for row in rows if row["result_class"] == "weak_hit"),
|
||||
"misses": sum(1 for row in rows if row["result_class"] == "miss"),
|
||||
"blocked_events": sum(1 for row in rows if row["result_class"] == "blocked"),
|
||||
"unsupported_domain_events": sum(1 for row in rows if row["result_class"] == "unsupported_domain"),
|
||||
"official_primary_events": sum(
|
||||
1 for row in rows if row["official_snapshot"].get("level") == "primary"
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
"scope": "historical_event_backtest",
|
||||
"summary": summary,
|
||||
"boundary": (
|
||||
"This report measures whether current strict routes can support supplied historical events. "
|
||||
"Unsupported domains and blocked routes must not be overstated as validated predictive accuracy."
|
||||
),
|
||||
"events": rows,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run reusable historical-event backtest")
|
||||
parser.add_argument("--input", required=True, help="Path to local backtest payload JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = build_report(_load_payload(args.input))
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+1541
-6
File diff suppressed because it is too large
Load Diff
+489
-42
@@ -41,6 +41,7 @@ import sys
|
||||
import os
|
||||
import csv
|
||||
import math
|
||||
import time
|
||||
import sqlite3
|
||||
import importlib.util
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
@@ -1020,6 +1021,12 @@ def _build_relationship_narrative_payload(relationship_strict):
|
||||
'boundaries': [
|
||||
'未完成 D1 + D9 + UL + dual dasha 交叉前,不得把单一关系信号写成高置信度婚姻结论。',
|
||||
],
|
||||
'monthly_frame': {
|
||||
'primary_state': {'value': 'blocked'},
|
||||
'manifestation_mode': {'value': 'blocked'},
|
||||
'friction_source': {'value': 'blocked'},
|
||||
'time_confidence': {'value': 'blocked'},
|
||||
},
|
||||
'markdown': (
|
||||
"### 婚恋严格裁决\n"
|
||||
"- 当前缺少 relationship strict evidence,无法生成高严谨婚恋 narrative。\n"
|
||||
@@ -1034,6 +1041,8 @@ def _build_relationship_narrative_payload(relationship_strict):
|
||||
secondary_context = secondary_context if isinstance(secondary_context, list) else []
|
||||
confidence_cap = relationship_strict.get('confidence_cap') or event_judgement.get('confidence_cap') or 'unknown'
|
||||
dominant_label = event_judgement.get('dominant_label') if isinstance(event_judgement, dict) else None
|
||||
monthly_frame = relationship_strict.get('monthly_adjudication_summary') if isinstance(relationship_strict, dict) else {}
|
||||
monthly_frame = monthly_frame if isinstance(monthly_frame, dict) else {}
|
||||
synastry = present.get('synastry_relationship_support') if isinstance(present, dict) else {}
|
||||
synastry_signals = synastry.get('signals') if isinstance(synastry, dict) else []
|
||||
synastry_signals = synastry_signals if isinstance(synastry_signals, list) else []
|
||||
@@ -1061,6 +1070,10 @@ def _build_relationship_narrative_payload(relationship_strict):
|
||||
strengths.append('protective kuta support 已被识别,可作为关系稳定性的次级支持语义。')
|
||||
if 'synastry_exception_mitigated' in secondary_context:
|
||||
strengths.append('存在 exception mitigation,说明部分 Dosha/不利匹配在传统规则里有缓解条件。')
|
||||
if monthly_frame.get('primary_state', {}).get('value'):
|
||||
strengths.append(f"月度主状态:{monthly_frame.get('primary_state', {}).get('value')}。")
|
||||
if monthly_frame.get('manifestation_mode', {}).get('value'):
|
||||
strengths.append(f"落地形式:{monthly_frame.get('manifestation_mode', {}).get('value')}。")
|
||||
|
||||
if confidence_cap in {'low', 'blocked'}:
|
||||
risks.append('当前 confidence cap 偏低,dual dasha / external timing / marriage convergence 至少有一层存在冲突或不足。')
|
||||
@@ -1074,10 +1087,14 @@ def _build_relationship_narrative_payload(relationship_strict):
|
||||
risks.append('相关婚恋行星尊贵度摩擦较高,关系推进时更容易出现磨损与反复确认。')
|
||||
if 'shadbala_component_gap' in secondary_context:
|
||||
risks.append('Shadbala 六分量还存在缺口,关系强弱结论需继续保守处理。')
|
||||
if monthly_frame.get('friction_source', {}).get('value'):
|
||||
risks.append(f"阻力来源:{monthly_frame.get('friction_source', {}).get('value')}。")
|
||||
|
||||
boundaries.append('婚恋高严谨模式至少需要 D1、D9、UL、Vimshottari 与 Narayana dual dasha 同时在场。')
|
||||
boundaries.append('protective kuta support、Mahendra、Stree Deergha 等合盘细信号只能辅助,不得越权抬升 legal_marriage。')
|
||||
boundaries.append('若 dual dasha、external timing 或 marriage convergence 冲突,必须明确降置信度,而不是把关系窗口包装成婚姻必然落地。')
|
||||
if monthly_frame.get('time_confidence', {}).get('value'):
|
||||
boundaries.append(f"时间置信度:{monthly_frame.get('time_confidence', {}).get('value')}。")
|
||||
if 'public_formalization_candidate' in secondary_context:
|
||||
boundaries.append('public_formalization_candidate 只表示公开化候选,不等于法律婚姻,不能越权替代 legal_marriage。')
|
||||
if synastry_signals:
|
||||
@@ -1100,6 +1117,10 @@ def _build_relationship_narrative_payload(relationship_strict):
|
||||
f"- dominant_label: {dominant_label or 'none'}",
|
||||
f"- confidence_cap: {confidence_cap}",
|
||||
f"- secondary_context: {secondary_context}",
|
||||
f"- 月度主状态: {monthly_frame.get('primary_state', {}).get('value') or 'blocked'}",
|
||||
f"- 落地形式: {monthly_frame.get('manifestation_mode', {}).get('value') or 'blocked'}",
|
||||
f"- 阻力来源: {monthly_frame.get('friction_source', {}).get('value') or 'blocked'}",
|
||||
f"- 时间置信度: {monthly_frame.get('time_confidence', {}).get('value') or 'blocked'}",
|
||||
'- strengths:',
|
||||
*[f" - {item}" for item in strengths],
|
||||
'- risks:',
|
||||
@@ -1113,10 +1134,155 @@ def _build_relationship_narrative_payload(relationship_strict):
|
||||
'strengths': strengths,
|
||||
'risks': risks,
|
||||
'boundaries': boundaries,
|
||||
'monthly_frame': {
|
||||
'primary_state': monthly_frame.get('primary_state') or {'value': 'blocked'},
|
||||
'manifestation_mode': monthly_frame.get('manifestation_mode') or {'value': 'blocked'},
|
||||
'friction_source': monthly_frame.get('friction_source') or {'value': 'blocked'},
|
||||
'time_confidence': monthly_frame.get('time_confidence') or {'value': 'blocked'},
|
||||
},
|
||||
'markdown': "\n".join(markdown_lines),
|
||||
}
|
||||
|
||||
|
||||
def _base_strict_narrative_payload(route_label, strict, *, fallback_headline, strengths, risks, boundaries):
|
||||
monthly_frame = strict.get('monthly_adjudication_summary') if isinstance(strict, dict) else {}
|
||||
monthly_frame = monthly_frame if isinstance(monthly_frame, dict) else {}
|
||||
event_judgement = strict.get('event_judgement') if isinstance(strict, dict) else {}
|
||||
confidence_cap = strict.get('confidence_cap') or event_judgement.get('confidence_cap') or 'unknown'
|
||||
dominant_label = event_judgement.get('dominant_label') if isinstance(event_judgement, dict) else None
|
||||
|
||||
if monthly_frame.get('primary_state', {}).get('value'):
|
||||
strengths = list(strengths) + [f"月度主状态:{monthly_frame.get('primary_state', {}).get('value')}。"]
|
||||
if monthly_frame.get('manifestation_mode', {}).get('value'):
|
||||
strengths = list(strengths) + [f"落地形式:{monthly_frame.get('manifestation_mode', {}).get('value')}。"]
|
||||
if monthly_frame.get('friction_source', {}).get('value'):
|
||||
risks = list(risks) + [f"阻力来源:{monthly_frame.get('friction_source', {}).get('value')}。"]
|
||||
if monthly_frame.get('time_confidence', {}).get('value'):
|
||||
boundaries = list(boundaries) + [f"时间置信度:{monthly_frame.get('time_confidence', {}).get('value')}。"]
|
||||
|
||||
markdown_lines = [
|
||||
f"### {route_label}严格裁决",
|
||||
f"- headline: {fallback_headline}",
|
||||
f"- dominant_label: {dominant_label or 'none'}",
|
||||
f"- confidence_cap: {confidence_cap}",
|
||||
f"- 月度主状态: {monthly_frame.get('primary_state', {}).get('value') or 'blocked'}",
|
||||
f"- 落地形式: {monthly_frame.get('manifestation_mode', {}).get('value') or 'blocked'}",
|
||||
f"- 阻力来源: {monthly_frame.get('friction_source', {}).get('value') or 'blocked'}",
|
||||
f"- 时间置信度: {monthly_frame.get('time_confidence', {}).get('value') or 'blocked'}",
|
||||
'- strengths:',
|
||||
*[f" - {item}" for item in strengths],
|
||||
'- risks:',
|
||||
*[f" - {item}" for item in risks],
|
||||
'- boundaries:',
|
||||
*[f" - {item}" for item in boundaries],
|
||||
]
|
||||
return {
|
||||
'headline': fallback_headline,
|
||||
'strengths': list(strengths),
|
||||
'risks': list(risks),
|
||||
'boundaries': list(boundaries),
|
||||
'monthly_frame': {
|
||||
'primary_state': monthly_frame.get('primary_state') or {'value': 'blocked'},
|
||||
'manifestation_mode': monthly_frame.get('manifestation_mode') or {'value': 'blocked'},
|
||||
'friction_source': monthly_frame.get('friction_source') or {'value': 'blocked'},
|
||||
'time_confidence': monthly_frame.get('time_confidence') or {'value': 'blocked'},
|
||||
},
|
||||
'markdown': "\n".join(markdown_lines),
|
||||
}
|
||||
|
||||
|
||||
def _build_career_narrative_payload(career_strict):
|
||||
if not isinstance(career_strict, dict) or not career_strict:
|
||||
return _base_strict_narrative_payload(
|
||||
'事业',
|
||||
{},
|
||||
fallback_headline='事业严格裁决证据尚未完成,当前不能生成高严谨事业叙事。',
|
||||
strengths=[],
|
||||
risks=['缺少 career strict workflow 的核心证据,事业正文需降级。'],
|
||||
boundaries=['未完成 D1、D10、A10、Vimshottari 与 Narayana 交叉前,不得把单一事业信号写成高置信度结论。'],
|
||||
)
|
||||
event_judgement = career_strict.get('event_judgement') if isinstance(career_strict, dict) else {}
|
||||
secondary_context = event_judgement.get('secondary_context') if isinstance(event_judgement, dict) else []
|
||||
secondary_context = secondary_context if isinstance(secondary_context, list) else []
|
||||
missing = career_strict.get('missing_evidence') or []
|
||||
strengths = []
|
||||
risks = []
|
||||
boundaries = [
|
||||
'事业高严谨模式至少需要 D1、D10、A10、Functional Benefic/Malefic、Vimshottari 与 Narayana dual dasha 同时在场。',
|
||||
'VedAstro 官方事件日可以给时间支撑,但不得越权改写本命 promise 与 strict workflow 的边界。',
|
||||
]
|
||||
if event_judgement.get('dominant_label') == 'career_status':
|
||||
strengths.append('事业 strict workflow 已形成主裁决标签,说明职业主题不是泛泛活跃,而是进入可判读窗口。')
|
||||
if 'a10_active' in secondary_context:
|
||||
strengths.append('A10/Karma Pada 已进入主链,说明事业结果会更偏向社会角色、职责承接或可见产出。')
|
||||
if 'amk_active' in secondary_context:
|
||||
strengths.append('Amatyakaraka 已进入主链,说明职业能力、上级关系或专业角色承担被明显放大。')
|
||||
if 'karakamsha_context' in secondary_context:
|
||||
strengths.append('Karakamsha 已提供职业志向语义,适合用来判断方向感而不只是短期机会。')
|
||||
if missing:
|
||||
risks.append(f"仍缺少关键层:{', '.join(str(item) for item in missing[:4])}。")
|
||||
if 'virodhargala_obstruction' in secondary_context:
|
||||
risks.append('事业主轴存在 Argala 阻滞,推进通常伴随现实牵制、流程卡顿或资源不顺。')
|
||||
if 'dignity_high_friction' in secondary_context:
|
||||
risks.append('相关事业行星尊贵度摩擦较高,机会不一定消失,但落地成本会明显上升。')
|
||||
if 'shadbala_component_gap' in secondary_context:
|
||||
risks.append('Shadbala 六分量仍有缺口,强弱结论需继续保守。')
|
||||
headline = '事业严格裁决已接入主链,当前结论将强制引用本命 promise、双重大运、官方时间窗与结构阻力。'
|
||||
return _base_strict_narrative_payload(
|
||||
'事业',
|
||||
career_strict,
|
||||
fallback_headline=headline,
|
||||
strengths=strengths,
|
||||
risks=risks,
|
||||
boundaries=boundaries,
|
||||
)
|
||||
|
||||
|
||||
def _build_finance_narrative_payload(finance_strict):
|
||||
if not isinstance(finance_strict, dict) or not finance_strict:
|
||||
return _base_strict_narrative_payload(
|
||||
'财富',
|
||||
{},
|
||||
fallback_headline='财富严格裁决证据尚未完成,当前不能生成高严谨财富叙事。',
|
||||
strengths=[],
|
||||
risks=['缺少 finance strict workflow 的核心证据,财富正文需降级。'],
|
||||
boundaries=['未完成 D2/D11、财富 promise、Vimshottari 与 Narayana 交叉前,不得把单一财富信号写成高置信度结论。'],
|
||||
)
|
||||
event_judgement = finance_strict.get('event_judgement') if isinstance(finance_strict, dict) else {}
|
||||
secondary_context = event_judgement.get('secondary_context') if isinstance(event_judgement, dict) else []
|
||||
secondary_context = secondary_context if isinstance(secondary_context, list) else []
|
||||
missing = finance_strict.get('missing_evidence') or []
|
||||
strengths = []
|
||||
risks = []
|
||||
boundaries = [
|
||||
'财富高严谨模式至少需要 D2/D11 或等价财富 promise 层、Functional Benefic/Malefic、Vimshottari 与 Narayana dual dasha 同时在场。',
|
||||
'官方财富日窗口只能帮助判断回款/交易/现金流节奏,不能单独替代本命财富 promise。',
|
||||
]
|
||||
if event_judgement.get('dominant_label') == 'income_growth':
|
||||
strengths.append('财富 strict workflow 已判到 income_growth,说明更偏向真实入账增长,而不是空泛的财运变好。')
|
||||
if event_judgement.get('dominant_label') == 'public_wealth_status':
|
||||
strengths.append('财富 strict workflow 已判到 public_wealth_status,说明更像项目回款、公开收入状态或外部可见的收益变化。')
|
||||
if 'ashtakavarga_wealth_support' in secondary_context:
|
||||
strengths.append('Ashtakavarga 财富桥接已进入主链,可作为兑现能力的次级支持。')
|
||||
if missing:
|
||||
risks.append(f"仍缺少关键层:{', '.join(str(item) for item in missing[:4])}。")
|
||||
if 'avayogi_active' in secondary_context:
|
||||
risks.append('Avayogi 风险已触发,说明某些看似有钱流动的窗口也可能伴随高代价或错误决策。')
|
||||
if 'sodhita_wealth_friction' in secondary_context or 'ashtakavarga_wealth_friction' in secondary_context:
|
||||
risks.append('财富桥接层已提示兑现摩擦,现金流并不等于可自由留存。')
|
||||
if 'shadbala_component_gap' in secondary_context:
|
||||
risks.append('Shadbala 六分量仍有缺口,财富强弱结论需继续保守。')
|
||||
headline = '财富严格裁决已接入主链,当前结论会强制区分收入兑现、现金流动作与风险摩擦。'
|
||||
return _base_strict_narrative_payload(
|
||||
'财富',
|
||||
finance_strict,
|
||||
fallback_headline=headline,
|
||||
strengths=strengths,
|
||||
risks=risks,
|
||||
boundaries=boundaries,
|
||||
)
|
||||
|
||||
|
||||
def _build_vedastro_overview_payload(modules):
|
||||
overview = modules.get('vedastro_range_scan_result') if isinstance(modules, dict) else {}
|
||||
if not isinstance(overview, dict):
|
||||
@@ -1150,19 +1316,167 @@ def _build_vedastro_overview_payload(modules):
|
||||
}
|
||||
|
||||
|
||||
def _full_reading_profiler_enabled(args) -> bool:
|
||||
if bool(getattr(args, 'profile_stages', False)):
|
||||
return True
|
||||
env_value = os.environ.get('JYOTISH_PROFILE_STAGES', '').strip().lower()
|
||||
return env_value in {'1', 'true', 'yes', 'on'}
|
||||
|
||||
|
||||
def _record_stage_timing(stage_timings, stage, started_at, *, enabled=False, status='ok', details=None):
|
||||
elapsed = round(time.perf_counter() - started_at, 4)
|
||||
entry = {
|
||||
'stage': stage,
|
||||
'elapsed_seconds': elapsed,
|
||||
'status': status,
|
||||
}
|
||||
if details:
|
||||
entry['details'] = details
|
||||
stage_timings.append(entry)
|
||||
if enabled:
|
||||
print(f"[full-reading stage] {stage}: {elapsed:.4f}s ({status})", file=sys.stderr)
|
||||
return entry
|
||||
|
||||
|
||||
def _build_unified_stage_contract(stage_timings):
|
||||
groups = {
|
||||
'local_core': [
|
||||
'core_chart_and_setup',
|
||||
'dasha_and_core_varga_stack',
|
||||
'advanced_interpretation_and_timing_layers',
|
||||
'dynamic_hooks',
|
||||
],
|
||||
'official_evidence': [
|
||||
'vedastro_official_snapshot',
|
||||
'vedastro_main_entry_overview',
|
||||
],
|
||||
'contract_and_prompt': [
|
||||
'strict_contracts',
|
||||
'guided_topics',
|
||||
'ai_prompt_pack',
|
||||
],
|
||||
}
|
||||
grouped_rows = []
|
||||
for group_name, stage_names in groups.items():
|
||||
matched = [row for row in stage_timings if row.get('stage') in stage_names]
|
||||
grouped_rows.append({
|
||||
'group': group_name,
|
||||
'stages': [row.get('stage') for row in matched],
|
||||
'elapsed_seconds': round(
|
||||
sum(float(row.get('elapsed_seconds', 0) or 0) for row in matched),
|
||||
4,
|
||||
),
|
||||
'execution_mode': (
|
||||
'sync_remote_heavy' if group_name == 'official_evidence'
|
||||
else 'sync_structuring' if group_name == 'contract_and_prompt'
|
||||
else 'sync_local'
|
||||
),
|
||||
})
|
||||
return {
|
||||
'stage_contract_version': 1,
|
||||
'stage_groups': grouped_rows,
|
||||
'cache_recommendations': {
|
||||
'api_chart_response': 'recommended',
|
||||
'official_full_snapshot_semantic': 'recommended',
|
||||
},
|
||||
'async_recommendations': {
|
||||
'chart_async_optional': True,
|
||||
'high_rigor_async_recommended': True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
STRICT_WORKFLOW_MODULE_MAP = {
|
||||
'relationship': 'relationship_strict_evidence',
|
||||
'career': 'career_strict_evidence',
|
||||
'finance': 'finance_strict_evidence',
|
||||
}
|
||||
|
||||
|
||||
def _compact_strict_workflow_contract(strict):
|
||||
if not isinstance(strict, dict) or not strict:
|
||||
return None
|
||||
return {
|
||||
'question_type': strict.get('question_type'),
|
||||
'confidence_cap': strict.get('confidence_cap'),
|
||||
'blocked': bool(strict.get('blocked')),
|
||||
'reason': strict.get('reason'),
|
||||
'required_evidence': strict.get('required_evidence') or [],
|
||||
'missing_evidence': strict.get('missing_evidence') or [],
|
||||
'official_primary_evidence': strict.get('official_primary_evidence') or {},
|
||||
'local_supplemental_evidence': strict.get('local_supplemental_evidence') or {},
|
||||
'fallback_used': strict.get('fallback_used') or [],
|
||||
'blocked_items': strict.get('blocked_items') or [],
|
||||
'conflicts': strict.get('conflicts') or [],
|
||||
'technique_audit_summary': strict.get('technique_audit_summary') or {},
|
||||
'adjudication_stages': strict.get('adjudication_stages') or {},
|
||||
'multi_reference_reading_summary': strict.get('multi_reference_reading_summary') or {},
|
||||
'monthly_adjudication_summary': strict.get('monthly_adjudication_summary') or {},
|
||||
'official_day_signal_summary': strict.get('official_day_signal_summary') or {},
|
||||
'strict_adjudication_bundle': strict.get('strict_adjudication_bundle') or {},
|
||||
'verdict': strict.get('verdict'),
|
||||
'dominant_label': strict.get('dominant_label'),
|
||||
'main_conflicts': strict.get('main_conflicts') or [],
|
||||
}
|
||||
|
||||
|
||||
def _extract_strict_workflow_contracts(modules):
|
||||
contracts = {}
|
||||
if not isinstance(modules, dict):
|
||||
return contracts
|
||||
for route, module_name in STRICT_WORKFLOW_MODULE_MAP.items():
|
||||
contract = _compact_strict_workflow_contract(modules.get(module_name))
|
||||
if contract:
|
||||
contracts[route] = contract
|
||||
return contracts
|
||||
|
||||
|
||||
def _preferred_strict_workflow_contract(contracts):
|
||||
if not isinstance(contracts, dict):
|
||||
return None, {}
|
||||
for route in ('relationship', 'career', 'finance'):
|
||||
contract = contracts.get(route)
|
||||
if isinstance(contract, dict) and contract:
|
||||
return route, contract
|
||||
return None, {}
|
||||
|
||||
|
||||
def _build_vedastro_official_full_snapshot_payload(modules):
|
||||
snapshot = modules.get('vedastro_official_full_snapshot') if isinstance(modules, dict) else {}
|
||||
strict_workflow_contracts = _extract_strict_workflow_contracts(modules)
|
||||
primary_route, primary_contract = _preferred_strict_workflow_contract(strict_workflow_contracts)
|
||||
official_primary_evidence = primary_contract.get('official_primary_evidence') if isinstance(primary_contract, dict) else {}
|
||||
local_supplemental_evidence = primary_contract.get('local_supplemental_evidence') if isinstance(primary_contract, dict) else {}
|
||||
fallback_used = primary_contract.get('fallback_used') if isinstance(primary_contract, dict) else []
|
||||
blocked_items = primary_contract.get('blocked_items') if isinstance(primary_contract, dict) else []
|
||||
conflicts = primary_contract.get('conflicts') if isinstance(primary_contract, dict) else []
|
||||
if not isinstance(snapshot, dict) or not snapshot:
|
||||
return {
|
||||
'status': 'blocked',
|
||||
'available': False,
|
||||
'operation': 'official_full_snapshot',
|
||||
'primary_source': 'vedastro_official',
|
||||
'strict_workflow_primary_route': primary_route,
|
||||
'strict_workflow_routes_available': list(strict_workflow_contracts.keys()),
|
||||
'strict_workflow_contracts': strict_workflow_contracts,
|
||||
'official_primary_evidence': official_primary_evidence or {},
|
||||
'local_supplemental_evidence': local_supplemental_evidence or {},
|
||||
'fallback_used': fallback_used or [],
|
||||
'blocked_items': blocked_items or [],
|
||||
'conflicts': conflicts or [],
|
||||
'boundary_note': 'VedAstro official full snapshot is not attached.',
|
||||
}
|
||||
manifest = snapshot.get('request_manifest') if isinstance(snapshot.get('request_manifest'), dict) else {}
|
||||
requests = manifest.get('requests') if isinstance(manifest.get('requests'), list) else []
|
||||
snapshot_sections = snapshot.get('snapshot_sections') if isinstance(snapshot.get('snapshot_sections'), dict) else {}
|
||||
metadata = snapshot.get('source_metadata') if isinstance(snapshot.get('source_metadata'), dict) else {}
|
||||
full_catalog = metadata.get('official_full_capability_catalog') if isinstance(metadata.get('official_full_capability_catalog'), dict) else {}
|
||||
dynamic_selection = full_catalog.get('dynamic_selection') if isinstance(full_catalog.get('dynamic_selection'), dict) else {}
|
||||
report_references = {
|
||||
theme: selection.get('report_reference')
|
||||
for theme, selection in dynamic_selection.items()
|
||||
if isinstance(selection, dict) and isinstance(selection.get('report_reference'), dict)
|
||||
}
|
||||
return {
|
||||
'status': snapshot.get('status') or 'blocked',
|
||||
'available': bool(snapshot.get('available')),
|
||||
@@ -1173,8 +1487,22 @@ def _build_vedastro_official_full_snapshot_payload(modules):
|
||||
'request_section_count': len(requests),
|
||||
'request_sections': [item.get('section') for item in requests if isinstance(item, dict)],
|
||||
'method_catalog': manifest.get('method_catalog') or {},
|
||||
'official_full_capability_catalog_status': full_catalog.get('status'),
|
||||
'official_full_capability_catalog_summary': full_catalog.get('summary') or {},
|
||||
'official_full_capability_catalog_coverage': full_catalog.get('coverage') or {},
|
||||
'official_full_capability_domain_routing': full_catalog.get('domain_routing') or {},
|
||||
'official_full_capability_dynamic_selection': dynamic_selection,
|
||||
'official_report_references': report_references,
|
||||
'user_visibility': snapshot.get('user_visibility') or 'backend_raw_evidence_not_direct_user_report',
|
||||
'source_metadata': snapshot.get('source_metadata') or {},
|
||||
'strict_workflow_primary_route': primary_route,
|
||||
'strict_workflow_routes_available': list(strict_workflow_contracts.keys()),
|
||||
'strict_workflow_contracts': strict_workflow_contracts,
|
||||
'official_primary_evidence': official_primary_evidence or {},
|
||||
'local_supplemental_evidence': local_supplemental_evidence or {},
|
||||
'fallback_used': fallback_used or [],
|
||||
'blocked_items': blocked_items or [],
|
||||
'conflicts': conflicts or [],
|
||||
'boundary_note': (
|
||||
snapshot.get('reason')
|
||||
or 'VedAstro official full snapshot is the primary raw evidence layer; user reports consume selected slices only.'
|
||||
@@ -1201,9 +1529,13 @@ def _build_ai_prompt_pack(report):
|
||||
oracle_progress = _oracle_progress_snapshot()
|
||||
technique_audit_table = _build_technique_audit_table(functional_layer, oracle_progress, modules)
|
||||
relationship_narrative = _build_relationship_narrative_payload(modules.get('relationship_strict_evidence'))
|
||||
career_narrative = _build_career_narrative_payload(modules.get('career_strict_evidence'))
|
||||
finance_narrative = _build_finance_narrative_payload(modules.get('finance_strict_evidence'))
|
||||
vimsopaka_semantic_summary = _build_vimsopaka_semantic_summary(modules.get('vimsopaka'))
|
||||
vedastro_overview = _build_vedastro_overview_payload(modules)
|
||||
vedastro_official_full_snapshot = _build_vedastro_official_full_snapshot_payload(modules)
|
||||
strict_workflow_contracts = _extract_strict_workflow_contracts(modules)
|
||||
strict_workflow_primary_route, primary_strict_contract = _preferred_strict_workflow_contract(strict_workflow_contracts)
|
||||
guided_topics = modules.get('guided_topics') if isinstance(modules.get('guided_topics'), list) else build_guided_topics(report)
|
||||
capability_evidence_pool = build_capability_evidence_pool_summary()
|
||||
|
||||
@@ -1276,12 +1608,22 @@ def _build_ai_prompt_pack(report):
|
||||
},
|
||||
'oracle_progress': oracle_progress,
|
||||
'functional_benefic_malefic': functional_layer,
|
||||
'strict_workflow_primary_route': strict_workflow_primary_route,
|
||||
'strict_workflow_routes_available': list(strict_workflow_contracts.keys()),
|
||||
'strict_workflow_contracts': strict_workflow_contracts,
|
||||
'official_primary_evidence': primary_strict_contract.get('official_primary_evidence') if isinstance(primary_strict_contract, dict) else {},
|
||||
'local_supplemental_evidence': primary_strict_contract.get('local_supplemental_evidence') if isinstance(primary_strict_contract, dict) else {},
|
||||
'fallback_used': primary_strict_contract.get('fallback_used') if isinstance(primary_strict_contract, dict) else [],
|
||||
'blocked_items': primary_strict_contract.get('blocked_items') if isinstance(primary_strict_contract, dict) else [],
|
||||
'conflicts': primary_strict_contract.get('conflicts') if isinstance(primary_strict_contract, dict) else [],
|
||||
'vedastro_official_full_snapshot': vedastro_official_full_snapshot,
|
||||
'vedastro_overview': vedastro_overview,
|
||||
'guided_topics': guided_topics,
|
||||
'capability_evidence_pool': capability_evidence_pool,
|
||||
'technique_audit_table': technique_audit_table,
|
||||
'career_narrative': career_narrative,
|
||||
'relationship_narrative': relationship_narrative,
|
||||
'finance_narrative': finance_narrative,
|
||||
'vimsopaka_semantic_summary': vimsopaka_semantic_summary,
|
||||
}
|
||||
|
||||
@@ -1386,6 +1728,8 @@ def _attach_vedastro_main_entry_overview(report, args):
|
||||
combined_events = []
|
||||
domain_statuses = {}
|
||||
top_events = {}
|
||||
daily_windows_by_domain = {}
|
||||
top_daily_window_by_domain = {}
|
||||
failure_reason = None
|
||||
availability = True
|
||||
|
||||
@@ -1404,6 +1748,12 @@ def _attach_vedastro_main_entry_overview(report, args):
|
||||
top_event = domain_report.get('top_event')
|
||||
if isinstance(top_event, dict):
|
||||
top_events[domain] = top_event
|
||||
daily_windows = domain_report.get('daily_windows')
|
||||
if isinstance(daily_windows, list):
|
||||
daily_windows_by_domain[domain] = daily_windows
|
||||
top_daily_window = domain_report.get('top_daily_window')
|
||||
if isinstance(top_daily_window, dict):
|
||||
top_daily_window_by_domain[domain] = top_daily_window
|
||||
|
||||
primary_status = next(
|
||||
(
|
||||
@@ -1447,6 +1797,18 @@ def _attach_vedastro_main_entry_overview(report, args):
|
||||
'event_count': len(combined_events),
|
||||
'top_event': top_events.get('marriage') or next(iter(top_events.values()), None),
|
||||
'top_events_by_domain': top_events,
|
||||
'daily_windows': [
|
||||
item
|
||||
for domain in ('career', 'marriage', 'wealth')
|
||||
for item in (daily_windows_by_domain.get(domain) or [])
|
||||
if isinstance(item, dict)
|
||||
],
|
||||
'top_daily_window': (
|
||||
top_daily_window_by_domain.get('marriage')
|
||||
or next(iter(top_daily_window_by_domain.values()), None)
|
||||
),
|
||||
'daily_windows_by_domain': daily_windows_by_domain,
|
||||
'top_daily_window_by_domain': top_daily_window_by_domain,
|
||||
'evidence_ledger': combined_events,
|
||||
'source_metadata': source_metadata,
|
||||
'reason': failure_reason,
|
||||
@@ -1500,7 +1862,7 @@ def _attach_vedastro_official_full_snapshot(report, args):
|
||||
return report
|
||||
|
||||
|
||||
def _load_relationship_strict_collector():
|
||||
def _load_strict_evidence_collector():
|
||||
try:
|
||||
from mcp_server import _collect_strict_evidence as collector
|
||||
return collector
|
||||
@@ -1988,6 +2350,12 @@ def _past_event_verify(chart: Dict, asc_idx: int, args) -> Dict:
|
||||
# ============================================================================
|
||||
def cmd_varga(args):
|
||||
if not HAS_SWE: return {"error": "swisseph未安装"}
|
||||
try:
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
from varga import calc_varga
|
||||
except ImportError as e:
|
||||
return {"error": f"varga模块导入失败: {e}"}
|
||||
|
||||
swe.set_ephe_path('')
|
||||
hd = _birth_hour_decimal(args.hour, args.minute, _arg_second(args)) - args.tz
|
||||
jd = swe.julday(args.year, args.month, args.day, hd)
|
||||
@@ -2001,31 +2369,18 @@ def cmd_varga(args):
|
||||
if 'Rahu' in natal: natal['Ketu'] = (natal['Rahu'] + 180) % 360
|
||||
asc_lon, _ = swe.houses(jd, args.lat, args.lon, b'A'); asc_deg = (asc_lon[0] - ayanamsa) % 360 # 恒星黄道
|
||||
|
||||
def navamsa(lon):
|
||||
si = int(lon / 30); d = lon - si * 30; ni = int(d / (30/9))
|
||||
# BPHS: movable(0,3,6,9)=same, fixed(1,4,7,10)=+4, dual(2,5,8,11)=+8
|
||||
if si % 3 == 0: # movable: 0,3,6,9
|
||||
start = si
|
||||
elif si % 3 == 1: # fixed: 1,4,7,10
|
||||
start = (si + 4) % 12
|
||||
else: # dual: 2,5,8,11
|
||||
start = (si + 8) % 12
|
||||
return SIGNS[(start + ni) % 12]
|
||||
|
||||
def dasamsa(lon):
|
||||
si = int(lon / 30); d = lon - si * 30; di = int(d / 3)
|
||||
# BPHS: odd signs(0,2,4,6,8,10)=same, even signs(1,3,5,7,9,11)=+8 (9th)
|
||||
start = si if si % 2 == 0 else (si + 8) % 12
|
||||
return SIGNS[(start + di) % 12]
|
||||
def short_varga_row(lon, div):
|
||||
row = calc_varga(lon, div)
|
||||
return {"sign": row["sign"], "sign_cn": SIGNS_CN[row["sign"]]}
|
||||
|
||||
result = {"birth_info": f"{args.year}-{args.month:02d}-{args.day:02d} {_birth_time_string(args.hour, args.minute, _arg_second(args))}", "divisional_charts": {}}
|
||||
if args.d9 or args.all:
|
||||
d9 = {"ascendant": navamsa(asc_deg)}
|
||||
for p, l in natal.items(): d9[p] = {"sign": navamsa(l), "sign_cn": SIGNS_CN[navamsa(l)]}
|
||||
d9 = {"ascendant": calc_varga(asc_deg, 9)["sign"]}
|
||||
for p, l in natal.items(): d9[p] = short_varga_row(l, 9)
|
||||
result["divisional_charts"]["D9_Navamsa"] = d9
|
||||
if args.d10 or args.all:
|
||||
d10 = {"ascendant": dasamsa(asc_deg)}
|
||||
for p, l in natal.items(): d10[p] = {"sign": dasamsa(l), "sign_cn": SIGNS_CN[dasamsa(l)]}
|
||||
d10 = {"ascendant": calc_varga(asc_deg, 10)["sign"]}
|
||||
for p, l in natal.items(): d10[p] = short_varga_row(l, 10)
|
||||
result["divisional_charts"]["D10_Dasamsa"] = d10
|
||||
if not result["divisional_charts"]: result["note"] = "请指定 --d9, --d10 或 --all"
|
||||
return result
|
||||
@@ -4166,7 +4521,9 @@ def cmd_full_reading(args):
|
||||
→ 综合报告输出
|
||||
"""
|
||||
import time
|
||||
t0 = time.time()
|
||||
t0 = time.perf_counter()
|
||||
stage_timings = []
|
||||
profile_stages = _full_reading_profiler_enabled(args)
|
||||
|
||||
def _build_whole_sign_houses(asc_index, planets_data):
|
||||
"""Build a compatibility house map for add-on modules.
|
||||
@@ -4223,6 +4580,7 @@ def cmd_full_reading(args):
|
||||
}
|
||||
|
||||
# ── Step 1: 核心星盘 ──
|
||||
stage_started = time.perf_counter()
|
||||
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(args)
|
||||
if chart is None:
|
||||
return {"error": "swisseph未安装,无法计算星盘"}
|
||||
@@ -4249,8 +4607,16 @@ def cmd_full_reading(args):
|
||||
for pn, pd in planets.items():
|
||||
if isinstance(pd, dict) and 'sign' in pd:
|
||||
planet_sign_indices[pn] = SIGNS.index(pd['sign']) if pd['sign'] in SIGNS else 0
|
||||
_record_stage_timing(
|
||||
stage_timings,
|
||||
'core_chart_and_setup',
|
||||
stage_started,
|
||||
enabled=profile_stages,
|
||||
details={'modules': ['chart', 'house_map']},
|
||||
)
|
||||
|
||||
# ── Step 1.5: Special Lagnas 特殊上升点 (v4.4.0) ──
|
||||
stage_started = time.perf_counter()
|
||||
try:
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
from special_lagnas import SpecialLagnasCalculator
|
||||
@@ -4768,8 +5134,17 @@ def cmd_full_reading(args):
|
||||
report['modules']['narayana_dasha'] = narayana_result
|
||||
except Exception as e:
|
||||
report['errors'].append(f"narayana-dasha: {e}")
|
||||
_record_stage_timing(
|
||||
stage_timings,
|
||||
'dasha_and_core_varga_stack',
|
||||
stage_started,
|
||||
enabled=profile_stages,
|
||||
status='error' if any(err.startswith(('special-lagnas:', 'dasha:', 'yoga:', 'varga-full:', 'vimsopaka:', 'varga-extended:', 'dispositor-chain+inter-chart:', 'tajika-yogas+sahams:', 'yogas-doshas:', 'tithi-lord:', 'pancha-pakshi:', 'rashi-tulya-navamsa:', 'marriage-counting:', 'bhrigu-pada-dasha:', 'muntha:', 'trimshamsa-d30:', 'prashna:', 'solar-return:', 'narayana-dasha:')) for err in report['errors']) else 'ok',
|
||||
details={'through_step': '4.13'},
|
||||
)
|
||||
|
||||
# ── Step 5: 精确相位 ──
|
||||
stage_started = time.perf_counter()
|
||||
try:
|
||||
from aspects import calc_all_aspects
|
||||
aspects_result = calc_all_aspects(planet_lons, asc_deg)
|
||||
@@ -5185,12 +5560,17 @@ def cmd_full_reading(args):
|
||||
report['modules']['d9_navamsa_expanded'] = d9_expanded
|
||||
except Exception as e:
|
||||
report['errors'].append(f"d9-expanded: {e}")
|
||||
_record_stage_timing(
|
||||
stage_timings,
|
||||
'advanced_interpretation_and_timing_layers',
|
||||
stage_started,
|
||||
enabled=profile_stages,
|
||||
status='error' if any(err.startswith(('aspects:', 'jaimini:', 'nakshatra-adv:', 'nakshatra-dasha:', 'argala:', 'tajika:', 'shadbala:', 'remedies:', 'avasthas:', 'ashtakavarga:', 'validate:', 'audit:', 'actionable-context:', 'congregation:', 'vivah-saham:', 'transit-multi-ref:', 'dasa-convergence:', 'd9-expanded:')) for err in report['errors']) else 'ok',
|
||||
details={'through_step': '19'},
|
||||
)
|
||||
|
||||
# ── 汇总 ──
|
||||
elapsed = round(time.time() - t0, 2)
|
||||
module_count = len(report['modules'])
|
||||
error_count = len(report['errors'])
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
# ── 生成动态引导 (Dynamic Hooks) ──
|
||||
try:
|
||||
report['dynamic_hooks'] = generate_life_stage_hooks(
|
||||
@@ -5203,41 +5583,107 @@ def cmd_full_reading(args):
|
||||
except Exception as e:
|
||||
report['dynamic_hooks'] = []
|
||||
report['errors'].append(f"hook_engine: {e}")
|
||||
_record_stage_timing(
|
||||
stage_timings,
|
||||
'dynamic_hooks',
|
||||
stage_started,
|
||||
enabled=profile_stages,
|
||||
status='error' if any(err.startswith('hook_engine:') for err in report['errors']) else 'ok',
|
||||
)
|
||||
|
||||
report['summary'] = {
|
||||
'elapsed_seconds': elapsed,
|
||||
'modules_computed': module_count,
|
||||
'errors': error_count,
|
||||
'status': 'complete' if error_count == 0 else f'{error_count} errors',
|
||||
'next_step': '⭐ v6.1.6: full-reading 已输出 transit_multi_reference(四参考点) + dasa_convergence(五系统交叉) + yogini_dasha + ashtottari_dasha + kalachakra_dasha + d9_navamsa_expanded。AI必须使用四参考点分析Transit,Dasa预测必须标注多系统收敛等级。',
|
||||
}
|
||||
|
||||
try:
|
||||
relationship_strict_collector = _load_relationship_strict_collector()
|
||||
report['modules']['relationship_strict_evidence'] = relationship_strict_collector('relationship', report)
|
||||
report['modules']['relationship_strict_evidence']['user_narrative'] = _build_relationship_narrative_payload(
|
||||
report['modules']['relationship_strict_evidence']
|
||||
)
|
||||
except Exception as e:
|
||||
report['errors'].append(f"relationship-strict-evidence: {e}")
|
||||
report['summary'] = {}
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
try:
|
||||
_attach_vedastro_official_full_snapshot(report, args)
|
||||
except Exception as e:
|
||||
report['warnings'].append(f"vedastro-official-full-snapshot: {e}")
|
||||
_record_stage_timing(
|
||||
stage_timings,
|
||||
'vedastro_official_snapshot',
|
||||
stage_started,
|
||||
enabled=profile_stages,
|
||||
status='warning' if any(warn.startswith('vedastro-official-full-snapshot:') for warn in report['warnings']) else 'ok',
|
||||
)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
try:
|
||||
strict_evidence_collector = _load_strict_evidence_collector()
|
||||
for route in ('relationship', 'career', 'finance'):
|
||||
module_name = STRICT_WORKFLOW_MODULE_MAP[route]
|
||||
report['modules'][module_name] = strict_evidence_collector(route, report)
|
||||
report['modules']['career_strict_evidence']['user_narrative'] = _build_career_narrative_payload(
|
||||
report['modules']['career_strict_evidence']
|
||||
)
|
||||
report['modules']['relationship_strict_evidence']['user_narrative'] = _build_relationship_narrative_payload(
|
||||
report['modules']['relationship_strict_evidence']
|
||||
)
|
||||
report['modules']['finance_strict_evidence']['user_narrative'] = _build_finance_narrative_payload(
|
||||
report['modules']['finance_strict_evidence']
|
||||
)
|
||||
except Exception as e:
|
||||
report['errors'].append(f"strict-evidence-collector: {e}")
|
||||
_record_stage_timing(
|
||||
stage_timings,
|
||||
'strict_contracts',
|
||||
stage_started,
|
||||
enabled=profile_stages,
|
||||
status='error' if any(err.startswith('strict-evidence-collector:') for err in report['errors']) else 'ok',
|
||||
)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
try:
|
||||
_attach_vedastro_main_entry_overview(report, args)
|
||||
except Exception as e:
|
||||
report['warnings'].append(f"vedastro-main-entry-overview: {e}")
|
||||
_record_stage_timing(
|
||||
stage_timings,
|
||||
'vedastro_main_entry_overview',
|
||||
stage_started,
|
||||
enabled=profile_stages,
|
||||
status='warning' if any(warn.startswith('vedastro-main-entry-overview:') for warn in report['warnings']) else 'ok',
|
||||
)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
try:
|
||||
report['modules']['guided_topics'] = build_guided_topics(report)
|
||||
except Exception as e:
|
||||
report['warnings'].append(f"guided-topics: {e}")
|
||||
report['modules']['guided_topics'] = []
|
||||
_record_stage_timing(
|
||||
stage_timings,
|
||||
'guided_topics',
|
||||
stage_started,
|
||||
enabled=profile_stages,
|
||||
status='warning' if any(warn.startswith('guided-topics:') for warn in report['warnings']) else 'ok',
|
||||
)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
report['ai_prompt_pack'] = _build_ai_prompt_pack(report)
|
||||
_record_stage_timing(
|
||||
stage_timings,
|
||||
'ai_prompt_pack',
|
||||
stage_started,
|
||||
enabled=profile_stages,
|
||||
)
|
||||
|
||||
elapsed = round(time.perf_counter() - t0, 4)
|
||||
module_count = len(report['modules'])
|
||||
error_count = len(report['errors'])
|
||||
slowest_stages = sorted(stage_timings, key=lambda item: item.get('elapsed_seconds', 0), reverse=True)[:5]
|
||||
unified_stage_contract = _build_unified_stage_contract(stage_timings)
|
||||
report['summary'] = {
|
||||
'elapsed_seconds': elapsed,
|
||||
'modules_computed': module_count,
|
||||
'errors': error_count,
|
||||
'status': 'complete' if error_count == 0 else f'{error_count} errors',
|
||||
'stage_timing_enabled': True,
|
||||
'stage_timings': stage_timings,
|
||||
'slowest_stages': slowest_stages,
|
||||
'guided_topics': report['modules'].get('guided_topics', []),
|
||||
**unified_stage_contract,
|
||||
'next_step': '⭐ v6.1.6: full-reading 已输出 transit_multi_reference(四参考点) + dasa_convergence(五系统交叉) + yogini_dasha + ashtottari_dasha + kalachakra_dasha + d9_navamsa_expanded。AI必须使用四参考点分析Transit,Dasa预测必须标注多系统收敛等级。',
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
@@ -5549,6 +5995,7 @@ def main():
|
||||
p.add_argument('--today', default=None, help='Dasha/Sandhi参考日期 YYYY-MM-DD(默认今天)')
|
||||
p.add_argument('--transit-date', default=None, help='Transit真实过境参考日期 YYYY-MM-DD(默认跟随--today或今天)')
|
||||
p.add_argument('--target-year', type=int, default=None, help='太阳返照盘目标年份(默认不计算 Varshaphala)')
|
||||
p.add_argument('--profile-stages', action='store_true', help='输出 full-reading 粗粒度阶段耗时,并在 summary 中附带 stage timings')
|
||||
|
||||
# 23. prashna (v3.9新增)
|
||||
p = sub.add_parser('prashna', help='Prashna问事占星(提问时刻星盘+Arudha+Sphuta+Sahams)')
|
||||
|
||||
+98
-18
@@ -134,27 +134,95 @@ def calc_narayana_antardasha(
|
||||
if md is None:
|
||||
return []
|
||||
|
||||
total_years = md['years']
|
||||
return _subdivide_narayana_period(
|
||||
mahadasha_periods=mahadasha_periods,
|
||||
parent_period=md,
|
||||
start_sign_idx=md_sign_idx,
|
||||
parent_key='parent_md',
|
||||
parent_name=SIGNS[md_sign_idx],
|
||||
)
|
||||
|
||||
|
||||
def calc_narayana_pratyantardasha(
|
||||
mahadasha_periods: List[Dict],
|
||||
antardasha_period: Dict,
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
计算给定 Antardasha 的 Pratyantardasha 子周期。
|
||||
|
||||
返回的 start_age / end_age 与 Mahadasha、Antardasha 使用同一条绝对年龄轴,
|
||||
方便 get_current_narayana_dasha 直接定位当前周期。
|
||||
"""
|
||||
if not antardasha_period:
|
||||
return []
|
||||
|
||||
sign_idx = antardasha_period.get('sign_idx')
|
||||
if sign_idx is None:
|
||||
return []
|
||||
|
||||
return _subdivide_narayana_period(
|
||||
mahadasha_periods=mahadasha_periods,
|
||||
parent_period=antardasha_period,
|
||||
start_sign_idx=sign_idx,
|
||||
parent_key='parent_ad',
|
||||
parent_name=antardasha_period.get('sign', SIGNS[sign_idx]),
|
||||
)
|
||||
|
||||
|
||||
def _subdivide_narayana_period(
|
||||
mahadasha_periods: List[Dict],
|
||||
parent_period: Dict,
|
||||
start_sign_idx: int,
|
||||
parent_key: str,
|
||||
parent_name: str,
|
||||
) -> List[Dict]:
|
||||
"""按 Narayana 星座年数权重切分父周期,返回绝对年龄轴上的子周期。"""
|
||||
if not mahadasha_periods:
|
||||
return []
|
||||
|
||||
period_by_sign = {p['sign_idx']: p for p in mahadasha_periods}
|
||||
denominator = sum(float(p.get('years', 0)) for p in mahadasha_periods)
|
||||
if denominator <= 0:
|
||||
return []
|
||||
|
||||
total_years = float(parent_period.get('years', 0))
|
||||
parent_start = float(parent_period.get('start_age', 0))
|
||||
parent_end = float(parent_period.get('end_age', parent_start + total_years))
|
||||
if len(period_by_sign) == 12:
|
||||
weighted_sequence = [period_by_sign[(start_sign_idx + i) % 12] for i in range(12)]
|
||||
else:
|
||||
start_pos = next(
|
||||
(i for i, p in enumerate(mahadasha_periods) if p.get('sign_idx') == start_sign_idx),
|
||||
0,
|
||||
)
|
||||
weighted_sequence = mahadasha_periods[start_pos:] + mahadasha_periods[:start_pos]
|
||||
|
||||
sub_periods = []
|
||||
cum = md['start_age']
|
||||
cum = parent_start
|
||||
sequence_len = len(weighted_sequence)
|
||||
|
||||
# Antardasha 从 MD 星座开始,按黄道序推进
|
||||
for i in range(12):
|
||||
sign_idx = (md_sign_idx + i) % 12
|
||||
lord_sign = mahadasha_periods[0]['lord_in_sign'] # placeholder
|
||||
sub_years_ratio = mahadasha_periods[i]['years'] / sum(p['years'] for p in mahadasha_periods)
|
||||
sub_years = round(total_years * sub_years_ratio, 2)
|
||||
for i, weighted_period in enumerate(weighted_sequence):
|
||||
sign_idx = weighted_period['sign_idx']
|
||||
|
||||
if i == sequence_len - 1:
|
||||
end_age = parent_end
|
||||
else:
|
||||
sub_years_raw = total_years * float(weighted_period.get('years', 0)) / denominator
|
||||
end_age = cum + sub_years_raw
|
||||
|
||||
start_age = cum
|
||||
years = max(0.0, end_age - start_age)
|
||||
sub_periods.append({
|
||||
'sign': SIGNS[sign_idx],
|
||||
'sign_idx': sign_idx,
|
||||
'lord': SIGN_LORDS[SIGNS[sign_idx]],
|
||||
'years': sub_years,
|
||||
'start_age': round(cum, 2),
|
||||
'end_age': round(cum + sub_years, 2),
|
||||
'parent_md': SIGNS[md_sign_idx],
|
||||
'years': round(years, 4),
|
||||
'start_age': round(start_age, 4),
|
||||
'end_age': round(end_age, 4),
|
||||
parent_key: parent_name,
|
||||
'sequence_index': i,
|
||||
})
|
||||
cum += sub_years
|
||||
cum = end_age
|
||||
|
||||
return sub_periods
|
||||
|
||||
@@ -181,7 +249,8 @@ def get_current_narayana_dasha(
|
||||
if total_cycle == 0:
|
||||
return result
|
||||
|
||||
age_in_cycle = current_age % total_cycle
|
||||
cycle_start = min(float(p.get('start_age', 0)) for p in mahadasha_periods)
|
||||
age_in_cycle = ((current_age - cycle_start) % total_cycle) + cycle_start
|
||||
|
||||
# 找当前 MD
|
||||
for p in mahadasha_periods:
|
||||
@@ -198,17 +267,28 @@ def get_current_narayana_dasha(
|
||||
|
||||
# 计算 AD
|
||||
ads = calc_narayana_antardasha(mahadasha_periods, p['sign_idx'])
|
||||
elapsed_in_md = age_in_cycle - p['start_age']
|
||||
for ad in ads:
|
||||
if ad['start_age'] <= elapsed_in_md < ad['end_age']:
|
||||
if ad['start_age'] <= age_in_cycle < ad['end_age']:
|
||||
result['ad'] = {
|
||||
'sign': ad['sign'],
|
||||
'sign_idx': ad['sign_idx'],
|
||||
'lord': ad['lord'],
|
||||
'years': ad['years'],
|
||||
'start_age': round(p['start_age'] + ad['start_age'], 2),
|
||||
'end_age': round(p['start_age'] + ad['end_age'], 2),
|
||||
'start_age': ad['start_age'],
|
||||
'end_age': ad['end_age'],
|
||||
}
|
||||
pds = calc_narayana_pratyantardasha(mahadasha_periods, ad)
|
||||
for pd in pds:
|
||||
if pd['start_age'] <= age_in_cycle < pd['end_age']:
|
||||
result['pd'] = {
|
||||
'sign': pd['sign'],
|
||||
'sign_idx': pd['sign_idx'],
|
||||
'lord': pd['lord'],
|
||||
'years': pd['years'],
|
||||
'start_age': pd['start_age'],
|
||||
'end_age': pd['end_age'],
|
||||
}
|
||||
break
|
||||
break
|
||||
break
|
||||
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preflight scan for fragment discipline, redundancy, and real-capability boundaries.
|
||||
|
||||
This script is intentionally stdlib-only and reuses existing project audits.
|
||||
It exists to enforce a "scan before work" rule for multi-window development,
|
||||
so high-value drafts, mirrors, and external-work-brain fragments do not get
|
||||
forgotten or confused with source truth.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT_DIR = ROOT / "scripts"
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
import oracle_boundary_audit # noqa: E402
|
||||
|
||||
|
||||
LOCAL_DRAFTS_DIR = ROOT / "docs" / "research" / "local_drafts" / "2026-06"
|
||||
EXTERNAL_WORK_BRAIN_DIR = Path("/Users/wuyongnaren/.gemini/antigravity-ide/brain")
|
||||
DISTRIBUTION_MIRROR_DIR = Path("/Users/wuyongnaren/.workbuddy/skills/jyotish-vedic-astrology")
|
||||
ORACLE_FILE = ROOT / "references" / "oracle" / "dasha_shadbala_oracle_cases.json"
|
||||
|
||||
|
||||
def _read_text(path: Path) -> str:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def _run_json_script(*args: str) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
[sys.executable, *args],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=120,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError(completed.stderr or completed.stdout or f"Failed: {' '.join(args)}")
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def _list_files(base: Path, patterns: tuple[str, ...]) -> list[str]:
|
||||
if not base.exists():
|
||||
return []
|
||||
items: list[str] = []
|
||||
for pattern in patterns:
|
||||
for path in base.rglob(pattern):
|
||||
if path.is_file():
|
||||
items.append(str(path))
|
||||
return sorted(set(items))
|
||||
|
||||
|
||||
def _summarize_local_drafts() -> list[dict[str, Any]]:
|
||||
if not LOCAL_DRAFTS_DIR.exists():
|
||||
return []
|
||||
preferred_tokens = (
|
||||
"reuse_audit",
|
||||
"three_fronts",
|
||||
"tajika",
|
||||
"shadbala",
|
||||
"dasha",
|
||||
"yogi",
|
||||
"fragment",
|
||||
"truth",
|
||||
"benchmark",
|
||||
)
|
||||
rows: list[dict[str, Any]] = []
|
||||
for path in sorted(LOCAL_DRAFTS_DIR.glob("*.md")):
|
||||
name = path.name
|
||||
if not any(token in name for token in preferred_tokens):
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
"category": "repo_local_draft",
|
||||
"path": str(path),
|
||||
"reason": "High-value draft not promoted into repo truth yet.",
|
||||
}
|
||||
)
|
||||
return rows[:12]
|
||||
|
||||
|
||||
def _summarize_external_work_brain() -> list[dict[str, Any]]:
|
||||
if not EXTERNAL_WORK_BRAIN_DIR.exists():
|
||||
return []
|
||||
rows: list[dict[str, Any]] = []
|
||||
for path in _list_files(
|
||||
EXTERNAL_WORK_BRAIN_DIR,
|
||||
("*.md", "*.py"),
|
||||
):
|
||||
name = os.path.basename(path).lower()
|
||||
if not any(token in name for token in ("vedastro", "audit", "skill", "workflow", "oracle")):
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
"category": "external_work_brain",
|
||||
"path": path,
|
||||
"reason": "Recovery-only work-brain artifact; re-anchor before reuse.",
|
||||
}
|
||||
)
|
||||
return rows[:12]
|
||||
|
||||
|
||||
def _redundant_or_mirror_rows(fragment_audit: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
rows = [
|
||||
{
|
||||
"category": "distribution_mirror",
|
||||
"path": str(DISTRIBUTION_MIRROR_DIR),
|
||||
"reason": "Historical skill mirror; do not reverse-sync over main repo truth.",
|
||||
}
|
||||
]
|
||||
for rel in fragment_audit.get("workspace_residue", {}).get("untracked_files", [])[:12]:
|
||||
rows.append(
|
||||
{
|
||||
"category": "workspace_residue",
|
||||
"path": str(ROOT / rel),
|
||||
"reason": "Untracked residue or generated artifact; review before treating as source truth.",
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _real_capability_risks(oracle_boundary: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
summary = oracle_boundary.get("summary", {})
|
||||
risks = [
|
||||
{
|
||||
"id": "external_oracle_not_closed",
|
||||
"severity": "high",
|
||||
"reason": "External oracle boundary remains open; production tuning is still blocked.",
|
||||
"evidence": {
|
||||
"production_tuning_recommended": summary.get("production_tuning_recommended"),
|
||||
"open_items": summary.get("open_items", []),
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "historical_event_accuracy_not_proven",
|
||||
"severity": "high",
|
||||
"reason": "Engineering surfaces are covered, but historical life-event accuracy is not yet proven by external oracle closure.",
|
||||
"evidence": {
|
||||
"routes_present": ["career_timing_strict", "event_verification_strict", "full_reading_strict"],
|
||||
"needs_real_backtest": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "official_vs_local_boundary",
|
||||
"severity": "medium",
|
||||
"reason": "VedAstro official ingestion exists, but some real-capability claims still depend on local fallback and incomplete external comparison packs.",
|
||||
"evidence": {
|
||||
"longitude_cases": summary.get("longitude_cases", 0),
|
||||
"dasha_cases": summary.get("dasha_cases", 0),
|
||||
"shadbala_cases": summary.get("shadbala_cases", 0),
|
||||
},
|
||||
},
|
||||
]
|
||||
return risks
|
||||
|
||||
|
||||
def _cleanup_priorities(fragment_audit: dict[str, Any], high_value_unpromoted: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
workspace_residue = fragment_audit.get("workspace_residue", {}).get("untracked_files", [])
|
||||
priorities = [
|
||||
{
|
||||
"id": "triage_workspace_residue",
|
||||
"severity": "high" if workspace_residue else "medium",
|
||||
"why": "Untracked residue can hide generated truth, stale artifacts, or partial experiments across windows.",
|
||||
"evidence": {
|
||||
"untracked_count": len(workspace_residue),
|
||||
"sample_paths": workspace_residue[:5],
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "promote_or_archive_high_value_drafts",
|
||||
"severity": "medium",
|
||||
"why": "High-value draft audits should either be promoted into repo truth or explicitly archived to reduce rediscovery cost.",
|
||||
"evidence": {
|
||||
"draft_count": len(high_value_unpromoted),
|
||||
"sample_paths": [item["path"] for item in high_value_unpromoted[:5]],
|
||||
},
|
||||
},
|
||||
]
|
||||
return priorities
|
||||
|
||||
|
||||
def build_report() -> dict[str, Any]:
|
||||
fragment_audit = _run_json_script("scripts/audit_fragments.py", "--strict")
|
||||
capability_audit = _run_json_script(
|
||||
"scripts/jyotish_engine.py",
|
||||
"audit-capabilities",
|
||||
"--mode",
|
||||
"validate",
|
||||
)
|
||||
oracle = json.loads(_read_text(ORACLE_FILE))
|
||||
oracle_boundary = oracle_boundary_audit.build_report(oracle)
|
||||
|
||||
high_value_unpromoted = _summarize_local_drafts() + _summarize_external_work_brain()
|
||||
redundant_or_mirror = _redundant_or_mirror_rows(fragment_audit)
|
||||
real_capability_risks = _real_capability_risks(oracle_boundary)
|
||||
cleanup_priorities = _cleanup_priorities(fragment_audit, high_value_unpromoted)
|
||||
|
||||
return {
|
||||
"scope": "preflight_fragment_scan",
|
||||
"summary": {
|
||||
"authority_layers_scanned": 4,
|
||||
"production_truth_layer": "main_repo_truth",
|
||||
"high_value_unpromoted_count": len(high_value_unpromoted),
|
||||
"redundant_or_mirror_count": len(redundant_or_mirror),
|
||||
"workspace_residue_count": len(fragment_audit.get("workspace_residue", {}).get("untracked_files", [])),
|
||||
"real_capability_risk_count": len(real_capability_risks),
|
||||
"real_capability_status": "engineering_surfaces_covered_but_external_accuracy_not_closed",
|
||||
},
|
||||
"layers": {
|
||||
"main_repo_truth": {
|
||||
"status": "authoritative",
|
||||
"paths": [
|
||||
str(ROOT / "SKILL.md"),
|
||||
str(ROOT / "AGENTS.md"),
|
||||
str(ROOT / "scripts"),
|
||||
str(ROOT / "tests"),
|
||||
str(ROOT / "references"),
|
||||
str(ROOT / "docs" / "research"),
|
||||
],
|
||||
},
|
||||
"repo_local_drafts": {
|
||||
"status": "draft_reference_only",
|
||||
"path": str(LOCAL_DRAFTS_DIR),
|
||||
},
|
||||
"external_work_brain": {
|
||||
"status": "recovery_reference_only",
|
||||
"path": str(EXTERNAL_WORK_BRAIN_DIR),
|
||||
},
|
||||
"distribution_mirror": {
|
||||
"status": "mirror_do_not_reverse_sync",
|
||||
"path": str(DISTRIBUTION_MIRROR_DIR),
|
||||
},
|
||||
},
|
||||
"upstream_audits": {
|
||||
"fragment_audit": {
|
||||
"valid": fragment_audit.get("valid"),
|
||||
"candidate_count": fragment_audit.get("fragments", {}).get("candidate_count"),
|
||||
"workspace_residue_count": fragment_audit.get("workspace_residue", {}).get("untracked_count"),
|
||||
},
|
||||
"capability_audit": {
|
||||
"valid": capability_audit.get("valid"),
|
||||
"technique_count": capability_audit.get("technique_count"),
|
||||
"problem_count": capability_audit.get("problem_count"),
|
||||
},
|
||||
},
|
||||
"findings": {
|
||||
"high_value_unpromoted_count": len(high_value_unpromoted),
|
||||
"redundant_or_mirror_count": len(redundant_or_mirror),
|
||||
"workspace_residue_count": len(fragment_audit.get("workspace_residue", {}).get("untracked_files", [])),
|
||||
"real_capability_risk_count": len(real_capability_risks),
|
||||
},
|
||||
"high_value_unpromoted": high_value_unpromoted,
|
||||
"redundant_or_mirror": redundant_or_mirror,
|
||||
"real_capability_boundary": {
|
||||
"status": "not_fully_closed",
|
||||
"oracle_boundary": {
|
||||
"scope": oracle_boundary.get("scope"),
|
||||
"production_tuning_recommended": oracle_boundary.get("summary", {}).get("production_tuning_recommended"),
|
||||
"dasha_cases": oracle_boundary.get("summary", {}).get("dasha_cases"),
|
||||
"shadbala_cases": oracle_boundary.get("summary", {}).get("shadbala_cases"),
|
||||
"longitude_cases": oracle_boundary.get("summary", {}).get("longitude_cases"),
|
||||
"open_items": oracle_boundary.get("summary", {}).get("open_items", []),
|
||||
},
|
||||
},
|
||||
"real_capability_risks": real_capability_risks,
|
||||
"cleanup_priorities": cleanup_priorities,
|
||||
"boundary": (
|
||||
"Run this preflight scan before major work so drafts, mirrors, and external-work-brain "
|
||||
"fragments are reviewed deliberately, and so engineering-surface success is not mistaken "
|
||||
"for externally validated historical accuracy."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(json.dumps(build_report(), ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+2
-2
@@ -330,9 +330,9 @@ def calc_sthana_bala(pname: str, lon: float, sign: str, house: int) -> Dict:
|
||||
d3_sign_idx = varga_map(sign_idx, d3_part, 3)
|
||||
d3_sign = VARGA_SIGNS[d3_sign_idx]
|
||||
if pname == VARGA_SIGN_LORDS.get(d3_sign, ''):
|
||||
d3_score = 45.0
|
||||
d3_score = 30.0
|
||||
elif d3_sign == exalt_sign:
|
||||
d3_score = 50.0
|
||||
d3_score = 45.0
|
||||
elif d3_sign == debilit_sign:
|
||||
d3_score = 5.0
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare candidate Dig Bala models against external oracle rows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT_DIR = ROOT / "scripts"
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
import jyotish_engine as engine # type: ignore
|
||||
import oracle_boundary_audit # type: ignore
|
||||
from shadbala import DIG_BALA_HOUSE, calc_dig_bala # type: ignore
|
||||
|
||||
|
||||
MODEL_NAMES = [
|
||||
"current_linear_house_model",
|
||||
"house_midpoint_angular_model",
|
||||
"bhava_madhya_angular_model",
|
||||
]
|
||||
|
||||
|
||||
def _load_oracle(path: str) -> dict[str, Any]:
|
||||
resolved = Path(path)
|
||||
if not resolved.is_absolute():
|
||||
resolved = ROOT / resolved
|
||||
return oracle_boundary_audit._load_oracle(str(resolved))
|
||||
|
||||
|
||||
def _iter_external_verified_template_cases(oracle: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
for case in oracle.get("template_cases", []):
|
||||
if case.get("status") == "external_verified" and isinstance(case.get("target", {}).get("shadbala_components"), dict):
|
||||
out.append(case)
|
||||
return out
|
||||
|
||||
|
||||
def _namespace_from_template(case: dict[str, Any]) -> Any:
|
||||
birth = case["birth"]
|
||||
settings = case.get("settings", {})
|
||||
return type(
|
||||
"Args",
|
||||
(),
|
||||
{
|
||||
"year": birth["year"],
|
||||
"month": birth["month"],
|
||||
"day": birth["day"],
|
||||
"hour": birth["hour"],
|
||||
"minute": birth.get("minute", 0),
|
||||
"second": birth.get("second", 0),
|
||||
"lat": birth["lat"],
|
||||
"lon": birth["lon"],
|
||||
"tz": birth["tz"],
|
||||
"ayanamsa": settings.get("ayanamsa", "lahiri"),
|
||||
"node_mode": settings.get("node_mode", "mean"),
|
||||
},
|
||||
)()
|
||||
|
||||
|
||||
def _planet_lon(chart: dict[str, Any], planet: str) -> float:
|
||||
return float(chart["planets"][planet]["degree_raw"])
|
||||
|
||||
|
||||
def _asc_lon(chart: dict[str, Any]) -> float:
|
||||
return float(chart["ascendant"]["degree_raw"])
|
||||
|
||||
|
||||
def _whole_sign_house_midpoint(asc_lon: float, house: int) -> float:
|
||||
base = (asc_lon + (house - 1) * 30) % 360
|
||||
return (base + 15) % 360
|
||||
|
||||
|
||||
def _angular_distance(a: float, b: float) -> float:
|
||||
diff = abs(a - b) % 360
|
||||
return min(diff, 360 - diff)
|
||||
|
||||
|
||||
def _dig_from_strong_point(planet_lon: float, strong_point_lon: float) -> float:
|
||||
shorter_arc = _angular_distance(planet_lon, strong_point_lon)
|
||||
return max(0.0, (180.0 - shorter_arc) / 3.0)
|
||||
|
||||
|
||||
def _best_house_midpoint_lon(asc_lon: float, planet: str) -> float:
|
||||
best_house = DIG_BALA_HOUSE.get(planet, 1)
|
||||
return _whole_sign_house_midpoint(asc_lon, best_house)
|
||||
|
||||
|
||||
def _best_bhava_madhya_lon(chart: dict[str, Any], planet: str) -> float:
|
||||
best_house = DIG_BALA_HOUSE.get(planet, 1)
|
||||
house_row = chart["houses"].get(f"house_{best_house}", {})
|
||||
return float(house_row.get("cusp_degree", 0.0))
|
||||
|
||||
|
||||
def build_report(oracle_file: str) -> dict[str, Any]:
|
||||
oracle = _load_oracle(oracle_file)
|
||||
cases = _iter_external_verified_template_cases(oracle)
|
||||
rows: list[dict[str, Any]] = []
|
||||
model_diffs: dict[str, list[float]] = {name: [] for name in MODEL_NAMES}
|
||||
|
||||
for case in cases:
|
||||
chart = engine.cmd_chart(_namespace_from_template(case))
|
||||
asc_lon = _asc_lon(chart)
|
||||
target_components = case["target"]["shadbala_components"]
|
||||
for planet, external_row in target_components.items():
|
||||
if planet not in chart["planets"] or not isinstance(external_row, dict):
|
||||
continue
|
||||
external_dig = external_row.get("dig")
|
||||
if not isinstance(external_dig, (int, float)):
|
||||
continue
|
||||
planet_lon = _planet_lon(chart, planet)
|
||||
house = int(chart["planets"][planet]["house"])
|
||||
current_linear = calc_dig_bala(planet, house) / 60.0
|
||||
house_midpoint = _dig_from_strong_point(planet_lon, _best_house_midpoint_lon(asc_lon, planet)) / 60.0
|
||||
bhava_madhya = _dig_from_strong_point(planet_lon, _best_bhava_madhya_lon(chart, planet)) / 60.0
|
||||
candidates = {
|
||||
"current_linear_house_model": current_linear,
|
||||
"house_midpoint_angular_model": house_midpoint,
|
||||
"bhava_madhya_angular_model": bhava_madhya,
|
||||
}
|
||||
diffs = {name: round(abs(value - float(external_dig)), 4) for name, value in candidates.items()}
|
||||
for name, diff in diffs.items():
|
||||
model_diffs[name].append(diff)
|
||||
rows.append(
|
||||
{
|
||||
"case_id": case.get("id") or case.get("case_id"),
|
||||
"planet": planet,
|
||||
"external_dig_rupa": float(external_dig),
|
||||
"house": house,
|
||||
"planet_lon": round(planet_lon, 4),
|
||||
"asc_lon": round(asc_lon, 4),
|
||||
"current_linear_house_model": round(current_linear, 4),
|
||||
"house_midpoint_angular_model": round(house_midpoint, 4),
|
||||
"bhava_madhya_angular_model": round(bhava_madhya, 4),
|
||||
"abs_diffs": diffs,
|
||||
}
|
||||
)
|
||||
|
||||
avg_diffs = {
|
||||
name: round(sum(values) / len(values), 4) if values else math.inf
|
||||
for name, values in model_diffs.items()
|
||||
}
|
||||
best_model = min(avg_diffs, key=avg_diffs.get) if rows else None
|
||||
|
||||
return {
|
||||
"scope": "shadbala_dig_source_of_truth_audit",
|
||||
"schema_version": 1,
|
||||
"candidate_models": MODEL_NAMES,
|
||||
"summary": {
|
||||
"case_count": len(cases),
|
||||
"row_count": len(rows),
|
||||
"best_model_by_avg_abs_diff": best_model,
|
||||
"avg_abs_diff_by_model": avg_diffs,
|
||||
},
|
||||
"rows": rows,
|
||||
"boundary": (
|
||||
"This audit compares three local Dig Bala candidate models against external oracle rows. "
|
||||
"It is diagnostic only and does not modify production scoring."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
report = build_report("references/oracle/dasha_shadbala_oracle_cases.json")
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shared orchestration contract for skill/MCP and web/API surfaces."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RouteDefinition:
|
||||
question_type: str
|
||||
primary_theme: str
|
||||
focus_techniques: list[str]
|
||||
display_label: str
|
||||
|
||||
|
||||
class UnifiedConsultationOrchestrator:
|
||||
"""Normalizes user intent and exposes a surface-agnostic workflow contract."""
|
||||
|
||||
NAME = "UnifiedConsultationOrchestrator"
|
||||
SOURCE_PRIORITY = {
|
||||
"mode": "vedastro_official_snapshot_first",
|
||||
"priority": [
|
||||
"vedastro_official_snapshot",
|
||||
"local_supplemental_modules",
|
||||
"local_fallback_only_when_official_blocked",
|
||||
],
|
||||
"boundary": (
|
||||
"Official VedAstro raw evidence is preferred; local modules supplement, "
|
||||
"cross-check, and fallback when official calls are blocked."
|
||||
),
|
||||
}
|
||||
_THEME_ALIASES = {
|
||||
"relationship": "marriage",
|
||||
"marriage": "marriage",
|
||||
"finance": "wealth",
|
||||
"money": "wealth",
|
||||
"wealth": "wealth",
|
||||
"career": "career",
|
||||
"health": "health",
|
||||
"spirituality": "spirituality",
|
||||
"事业": "career",
|
||||
"婚恋": "marriage",
|
||||
"婚姻": "marriage",
|
||||
"感情": "marriage",
|
||||
"财富": "wealth",
|
||||
"财运": "wealth",
|
||||
"健康": "health",
|
||||
"灵性": "spirituality",
|
||||
}
|
||||
_DEFAULT_THEMES = ["career", "marriage", "wealth"]
|
||||
_ALLOWED_THEMES = {"career", "marriage", "wealth", "health", "spirituality"}
|
||||
_ROUTE_DEFINITIONS = {
|
||||
"career": RouteDefinition(
|
||||
question_type="career",
|
||||
primary_theme="career",
|
||||
focus_techniques=["D10", "Dasha", "Shadbala", "Transit", "Narayana Dasha"],
|
||||
display_label="career",
|
||||
),
|
||||
"relationship": RouteDefinition(
|
||||
question_type="relationship",
|
||||
primary_theme="marriage",
|
||||
focus_techniques=["D9", "UL Upapada", "Dasha", "Nakshatra", "Vivah Saham"],
|
||||
display_label="relationship",
|
||||
),
|
||||
"finance": RouteDefinition(
|
||||
question_type="finance",
|
||||
primary_theme="wealth",
|
||||
focus_techniques=["D2", "D11", "Dasha", "Shadbala", "Ashtakavarga"],
|
||||
display_label="finance",
|
||||
),
|
||||
"timing": RouteDefinition(
|
||||
question_type="timing",
|
||||
primary_theme="career",
|
||||
focus_techniques=["Dasha", "Transit", "Double Transit", "Gochara"],
|
||||
display_label="timing",
|
||||
),
|
||||
"general": RouteDefinition(
|
||||
question_type="general",
|
||||
primary_theme="career",
|
||||
focus_techniques=["D1", "D9", "Dasha", "Yoga", "Shadbala", "Ashtakavarga"],
|
||||
display_label="general",
|
||||
),
|
||||
}
|
||||
_SYNC_STEPS_BY_ROUTE = {
|
||||
"career": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
|
||||
"relationship": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
|
||||
"finance": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
|
||||
"timing": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
|
||||
"general": ["compute_chart", "run_rectification_gate", "run_thematic_report"],
|
||||
}
|
||||
_ASYNC_CANDIDATES = [
|
||||
"historical_event_backtest",
|
||||
"official_event_radar_expansion",
|
||||
"extended_prompt_pack_refresh",
|
||||
]
|
||||
|
||||
def normalize_themes(self, raw: Any) -> list[str]:
|
||||
if raw in (None, "", "all"):
|
||||
values = list(self._DEFAULT_THEMES)
|
||||
elif isinstance(raw, str):
|
||||
values = [raw]
|
||||
elif isinstance(raw, list):
|
||||
values = raw
|
||||
else:
|
||||
raise ValueError("theme/themes must be a string, list, or all")
|
||||
|
||||
normalized: list[str] = []
|
||||
for value in values:
|
||||
key = self._THEME_ALIASES.get(str(value).strip().lower(), str(value).strip().lower())
|
||||
if key not in self._ALLOWED_THEMES:
|
||||
raise ValueError(f"Unknown theme: {value}")
|
||||
if key not in normalized:
|
||||
normalized.append(key)
|
||||
return normalized or list(self._DEFAULT_THEMES)
|
||||
|
||||
def resolve_route(self, question: str, themes: list[str] | None = None) -> dict[str, Any]:
|
||||
text = (question or "").lower()
|
||||
normalized_themes = themes or list(self._DEFAULT_THEMES)
|
||||
|
||||
domain_tokens = {
|
||||
"career": ("career", "job", "work", "promotion", "business", "profession", "事业", "工作", "升职", "生意"),
|
||||
"relationship": ("marriage", "married", "wedding", "relationship", "love", "spouse", "partner", "divorce", "婚恋", "婚姻", "感情", "配偶", "恋爱", "结婚", "marry"),
|
||||
"finance": ("money", "wealth", "finance", "investment", "property", "income", "财务", "财富", "投资", "房产", "收入"),
|
||||
}
|
||||
first_hits: list[tuple[int, str]] = []
|
||||
for route_name, tokens in domain_tokens.items():
|
||||
indexes = [text.find(token) for token in tokens if token in text]
|
||||
indexes = [idx for idx in indexes if idx >= 0]
|
||||
if indexes:
|
||||
first_hits.append((min(indexes), route_name))
|
||||
|
||||
if first_hits:
|
||||
route_name = sorted(first_hits, key=lambda item: item[0])[0][1]
|
||||
route = self._ROUTE_DEFINITIONS[route_name]
|
||||
elif not text.strip():
|
||||
route = self._ROUTE_DEFINITIONS["general"]
|
||||
elif any(token in text for token in ("when", "timing", "event", "prediction", "future", "应期", "预测", "何时", "将来")):
|
||||
route = self._ROUTE_DEFINITIONS["timing"]
|
||||
elif "career" in normalized_themes:
|
||||
route = self._ROUTE_DEFINITIONS["career"]
|
||||
elif "marriage" in normalized_themes:
|
||||
route = self._ROUTE_DEFINITIONS["relationship"]
|
||||
elif "wealth" in normalized_themes:
|
||||
route = self._ROUTE_DEFINITIONS["finance"]
|
||||
else:
|
||||
route = self._ROUTE_DEFINITIONS["general"]
|
||||
|
||||
return {
|
||||
"question_type": route.question_type,
|
||||
"primary_theme": route.primary_theme,
|
||||
"focus_techniques": list(route.focus_techniques),
|
||||
"display_label": route.display_label,
|
||||
}
|
||||
|
||||
def shared_contract(
|
||||
self,
|
||||
*,
|
||||
entry_mode: str,
|
||||
question: str,
|
||||
themes: list[str],
|
||||
route_packet: dict[str, Any],
|
||||
surface: str,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.NAME,
|
||||
"surface": surface,
|
||||
"entry_mode": entry_mode,
|
||||
"question": question or "",
|
||||
"themes": list(themes),
|
||||
"route": dict(route_packet),
|
||||
"source_priority": {
|
||||
"mode": self.SOURCE_PRIORITY["mode"],
|
||||
"priority": list(self.SOURCE_PRIORITY["priority"]),
|
||||
"boundary": self.SOURCE_PRIORITY["boundary"],
|
||||
},
|
||||
"shared_capabilities": [
|
||||
"theme_normalization",
|
||||
"question_routing",
|
||||
"vedastro_official_priority",
|
||||
"rectification_gate_reuse",
|
||||
"thematic_report_reuse",
|
||||
],
|
||||
}
|
||||
|
||||
def runtime_planner(
|
||||
self,
|
||||
*,
|
||||
entry_mode: str,
|
||||
question: str,
|
||||
themes: list[str],
|
||||
route_packet: dict[str, Any],
|
||||
events: list[dict[str, Any]] | None,
|
||||
surface: str,
|
||||
high_rigor: bool,
|
||||
) -> dict[str, Any]:
|
||||
route_name = route_packet.get("question_type") or "general"
|
||||
sync_steps = list(self._SYNC_STEPS_BY_ROUTE.get(route_name, self._SYNC_STEPS_BY_ROUTE["general"]))
|
||||
if entry_mode == "rectification":
|
||||
sync_steps = [step for step in sync_steps if step != "run_rectification_gate"]
|
||||
sync_steps.insert(0, "run_rectification_gate")
|
||||
if high_rigor and "run_historical_event_backtest" not in sync_steps and events:
|
||||
sync_steps.append("run_historical_event_backtest")
|
||||
|
||||
async_candidates = list(self._ASYNC_CANDIDATES)
|
||||
if not events:
|
||||
async_candidates = [step for step in async_candidates if step != "historical_event_backtest"]
|
||||
|
||||
return {
|
||||
"planner_name": "UnifiedConsultationRuntimePlanner",
|
||||
"surface": surface,
|
||||
"entry_mode": entry_mode,
|
||||
"high_rigor": bool(high_rigor),
|
||||
"route": dict(route_packet),
|
||||
"question_context": {
|
||||
"question": question or "",
|
||||
"themes": list(themes),
|
||||
"event_count": len(events or []),
|
||||
},
|
||||
"sync_steps": sync_steps,
|
||||
"async_candidates": async_candidates,
|
||||
"source_priority": {
|
||||
"mode": self.SOURCE_PRIORITY["mode"],
|
||||
"priority": list(self.SOURCE_PRIORITY["priority"]),
|
||||
"boundary": self.SOURCE_PRIORITY["boundary"],
|
||||
},
|
||||
"reuse_contract": {
|
||||
"chart": "compute_chart",
|
||||
"rectification": "rectification_gate",
|
||||
"thematic_report": "thematic_report",
|
||||
"historical_backtest": "historical_event_backtest",
|
||||
},
|
||||
"boundary": (
|
||||
"This runtime planner unifies entry routing and module reuse. It does not imply that every VedAstro "
|
||||
"catalog method executes on every request; route-relevant official evidence is still subject to live "
|
||||
"availability, cache policy, and async limits."
|
||||
),
|
||||
}
|
||||
@@ -14,6 +14,11 @@ BPHS 分盘与 Ashtakavarga 独立验证脚本
|
||||
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
|
||||
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
|
||||
@@ -24,52 +29,12 @@ def sign_idx(name): return SIGNS.index(name) if name in SIGNS else -1
|
||||
# =============================================================================
|
||||
# 1. Navamsa (D9) 验证 - BPHS Chapter 6
|
||||
# =============================================================================
|
||||
# BPHS 标准:
|
||||
# 本仓采用的 JHora/BPHS Navamsa 口径:
|
||||
# - Movable signs (白羊0, 巨蟹3, 天秤6, 摩羯9): 从本星座开始
|
||||
# - Fixed signs (金牛1, 狮子4, 天蝎7, 水瓶10): 从第5星座开始 (+4)
|
||||
# - Dual signs (双子2, 处女5, 射手8, 双鱼11): 从第9星座开始 (+8)
|
||||
# - Fixed signs (金牛1, 狮子4, 天蝎7, 水瓶10): 从第9星座开始 (+8)
|
||||
# - Dual signs (双子2, 处女5, 射手8, 双鱼11): 从第5星座开始 (+4)
|
||||
# 每份 = 30/9 = 3.333... 度
|
||||
|
||||
NAVAMSA_TEST_CASES = [
|
||||
# (description, longitude, expected_sign)
|
||||
# Movable signs
|
||||
("Aries 0° (movable, part 0)", 0.0, "Aries"),
|
||||
("Aries 3.33° (movable, part 1)", 3.3333, "Taurus"),
|
||||
("Aries 10° (movable, part 3)", 10.0, "Cancer"),
|
||||
("Aries 16.67° (movable, part 5)", 16.6667, "Virgo"),
|
||||
("Aries 23.33° (movable, part 7)", 23.3333, "Scorpio"),
|
||||
("Aries 28° (movable, part 8)", 28.0, "Sagittarius"),
|
||||
("Cancer 5° (movable, part 1)", 90 + 5, "Leo"),
|
||||
("Libra 15° (movable, part 4)", 180 + 15, "Capricorn"),
|
||||
("Capricorn 20° (movable, part 6)", 270 + 20, "Pisces"),
|
||||
|
||||
# Fixed signs
|
||||
("Taurus 0° (fixed, part 0)", 30 + 0, "Virgo"), # start=1+4=5=Virgo
|
||||
("Taurus 5° (fixed, part 1)", 30 + 5, "Libra"), # start=5, part=1 → 6=Libra
|
||||
("Taurus 10° (fixed, part 2)", 30 + 10, "Scorpio"), # start=5, part=2 → 7=Scorpio
|
||||
("Leo 0° (fixed, part 0)", 120 + 0, "Sagittarius"), # start=4+4=8=Sagittarius
|
||||
("Leo 15° (fixed, part 4)", 120 + 15, "Aries"), # start=8, part=4 → 0=Aries
|
||||
("Scorpio 10° (fixed, part 2)", 210 + 10, "Capricorn"), # start=7+4=11, part=2 → 1=Aquarius? wait
|
||||
# Let me recalculate: Scorpio=7, start=7+4=11=Aquarius, part=int(10/3.333)=3, result=(11+3)%12=2=Gemini
|
||||
# Hmm wait, 10/3.333 = 3.0, so part=3. (11+3)%12=2=Gemini. But my test case says part 2. Let me fix.
|
||||
("Scorpio 6° (fixed, part 1)", 210 + 6, "Pisces"), # start=11, part=1 → 0=Aries... wait
|
||||
# 6/3.333 = 1.8, int=1. (11+1)%12=0=Aries. Let me recalculate.
|
||||
# Actually let me be more careful.
|
||||
("Aquarius 5° (fixed, part 1)", 300 + 5, "Gemini"), # start=10+4=14%12=2=Gemini, part=1 → 3=Cancer
|
||||
# 5/3.333=1.5, int=1. (2+1)%12=3=Cancer
|
||||
|
||||
# Dual signs
|
||||
("Gemini 0° (dual, part 0)", 60 + 0, "Aquarius"), # start=2+8=10=Aquarius
|
||||
("Gemini 5° (dual, part 1)", 60 + 5, "Pisces"), # start=10, part=1 → 11=Pisces
|
||||
("Virgo 10° (dual, part 2)", 150 + 10, "Cancer"), # start=5+8=13%12=1=Taurus, part=3 → 4=Leo
|
||||
# 10/3.333=3.0, int=3. (1+3)%12=4=Leo. Hmm test case says part 2. Let me fix.
|
||||
("Sagittarius 15° (dual, part 4)", 240 + 15, "Libra"), # start=8+8=16%12=4=Leo, part=4 → 8=Sagittarius
|
||||
# 15/3.333=4.5, int=4. (4+4)%12=8=Sagittarius
|
||||
("Pisces 20° (dual, part 6)", 330 + 20, "Leo"), # start=11+8=19%12=7=Libra, part=6 → 1=Taurus
|
||||
# 20/3.333=6.0, int=6. (7+6)%12=1=Taurus
|
||||
]
|
||||
|
||||
# Let me rewrite the test cases more carefully
|
||||
def calc_navamsa_ref(lon):
|
||||
"""BPHS标准navamsa - 参考实现"""
|
||||
si = int(lon / 30)
|
||||
@@ -78,9 +43,9 @@ def calc_navamsa_ref(lon):
|
||||
if si % 3 == 0: # movable
|
||||
start = si
|
||||
elif si % 3 == 1: # fixed
|
||||
start = (si + 4) % 12
|
||||
else: # dual
|
||||
start = (si + 8) % 12
|
||||
else: # dual
|
||||
start = (si + 4) % 12
|
||||
return (start + ni) % 12
|
||||
|
||||
NAVAMSA_TEST_CASES = [
|
||||
@@ -94,24 +59,17 @@ NAVAMSA_TEST_CASES = [
|
||||
("Cancer 5° → Leo (movable, part 1)", 90 + 5, 4),
|
||||
("Libra 15° → Capricorn (movable, part 4)", 180 + 15, 9),
|
||||
("Capricorn 20° → Pisces (movable, part 6)", 270 + 20, 11),
|
||||
|
||||
# Fixed signs (si % 3 == 1): start = (si + 4) % 12
|
||||
("Taurus 0° → Virgo (fixed, part 0, start=5)", 30 + 0, 5),
|
||||
("Taurus 5° → Libra (fixed, part 1, start=5)", 30 + 5, 6),
|
||||
("Taurus 10° → Scorpio (fixed, part 2, start=5)", 30 + 10, 7),
|
||||
("Taurus 20° → Capricorn (fixed, part 6, start=5)", 30 + 20, 11), # 20/3.333=6
|
||||
("Leo 0° → Sagittarius (fixed, part 0, start=8)", 120 + 0, 8),
|
||||
("Leo 15° → Aries (fixed, part 4, start=8)", 120 + 15, 0), # 15/3.333=4.5→4, (8+4)%12=0
|
||||
("Scorpio 6° → Aquarius (fixed, part 1, start=11)", 210 + 6, 0), # 6/3.333=1.8→1, (11+1)%12=0=Aries... wait
|
||||
# Let me recalculate: Scorpio=7, start=(7+4)%12=11=Aquarius. 6°/(30/9)=6/3.333=1.8, int=1. (11+1)%12=0=Aries
|
||||
# Hmm my expected was Aquarius. Let me trace more carefully.
|
||||
# Actually I think I made an error. Let me recalculate:
|
||||
# Scorpio = sign 7 (210-240°). 6° into Scorpio = 216° total.
|
||||
# part = int(6 / 3.333) = int(1.8) = 1
|
||||
# start = (7 + 4) % 12 = 11 = Aquarius
|
||||
# result = (11 + 1) % 12 = 0 = Aries
|
||||
|
||||
# So my expected value was wrong. Let me fix all the test cases by computing them properly.
|
||||
|
||||
# Fixed signs (si % 3 == 1): start = 9th from sign (+8)
|
||||
("Taurus 0° → Capricorn (fixed, part 0, start=9)", 30 + 0, 9),
|
||||
("Taurus 5° → Aquarius (fixed, part 1, start=9)", 30 + 5, 10),
|
||||
("Taurus 10° → Pisces (fixed, part 3, start=9)", 30 + 10, 0),
|
||||
("Leo 0° → Aries (fixed, part 0, start=0)", 120 + 0, 0),
|
||||
|
||||
# Dual signs (si % 3 == 2): start = 5th from sign (+4)
|
||||
("Gemini 0° → Libra (dual, part 0, start=6)", 60 + 0, 6),
|
||||
("Gemini 5° → Scorpio (dual, part 1, start=6)", 60 + 5, 7),
|
||||
("Virgo 10° → Leo (dual, part 3, start=1)", 150 + 10, 4),
|
||||
]
|
||||
|
||||
# I'll generate test cases programmatically to avoid manual errors
|
||||
@@ -147,12 +105,11 @@ def navamsa_ref(lon):
|
||||
d = lon - si * 30
|
||||
ni = int(d / (30 / 9))
|
||||
if si % 3 == 0: start = si
|
||||
elif si % 3 == 1: start = (si + 4) % 12
|
||||
else: start = (si + 8) % 12
|
||||
elif si % 3 == 1: start = (si + 8) % 12
|
||||
else: start = (si + 4) % 12
|
||||
return (start + ni) % 12
|
||||
|
||||
# 从 varga.py 导入
|
||||
sys.path.insert(0, '/Users/wuyongnaren/.workbuddy/skills/jyotish-vedic-astrology/scripts')
|
||||
# 从本仓 scripts/varga.py 导入
|
||||
from varga import calc_varga
|
||||
|
||||
# 全面测试:每个星座的 0°, 5°, 10°, 15°, 20°, 25°
|
||||
@@ -237,8 +194,8 @@ SIGNS_JE = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
|
||||
def navamsa_je(lon):
|
||||
si = int(lon / 30); d = lon - si * 30; ni = int(d / (30/9))
|
||||
if si % 3 == 0: start = si
|
||||
elif si % 3 == 1: start = (si + 4) % 12
|
||||
else: start = (si + 8) % 12
|
||||
elif si % 3 == 1: start = (si + 8) % 12
|
||||
else: start = (si + 4) % 12
|
||||
return SIGNS_JE[(start + ni) % 12]
|
||||
|
||||
def dasamsa_je(lon):
|
||||
|
||||
+74
-6
@@ -33,6 +33,15 @@ VARGA_META = {
|
||||
def _si(lon): return int(lon/30)%12
|
||||
def _sn(i): return SIGNS[i%12]
|
||||
def _odd(si): return si%2==0 # Aries(0)=odd
|
||||
def _modality(si):
|
||||
if si % 3 == 0:
|
||||
return 'movable'
|
||||
if si % 3 == 1:
|
||||
return 'fixed'
|
||||
return 'dual'
|
||||
|
||||
def _element(si):
|
||||
return si % 4
|
||||
|
||||
def _d30_map(si, pi):
|
||||
if _odd(si):
|
||||
@@ -73,9 +82,68 @@ def varga_map(si, pi, div):
|
||||
if div==60: return (si+pi)%12 if o else (si+1+pi)%12
|
||||
raise ValueError(f"不支持的D{div}")
|
||||
|
||||
def calc_varga(lon, div):
|
||||
|
||||
def _d30_map_vedastro(si, pi):
|
||||
if _odd(si):
|
||||
if pi < 5: return 7
|
||||
if pi < 10: return 10
|
||||
if pi < 18: return 8
|
||||
if pi < 25: return 2
|
||||
return 6
|
||||
else:
|
||||
if pi < 5: return 1
|
||||
if pi < 12: return 2
|
||||
if pi < 20: return 8
|
||||
if pi < 25: return 9
|
||||
return 7
|
||||
|
||||
|
||||
def varga_map_vedastro(si, pi, div):
|
||||
"""VedAstro-compatible varga sign mapping.
|
||||
|
||||
This mode is calibrated against VedAstro official AllPlanetData /
|
||||
AllHouseData outputs. It intentionally lives beside the historical local
|
||||
mapping so older research workflows can still audit classical variants.
|
||||
"""
|
||||
if div == 2:
|
||||
return (si + pi * 4) % 12
|
||||
if div == 4:
|
||||
return (si + pi * 3) % 12
|
||||
if div == 7:
|
||||
return (si + pi) % 12
|
||||
if div == 16:
|
||||
start = {'movable': 0, 'fixed': 4, 'dual': 8}[_modality(si)]
|
||||
return (start + pi) % 12
|
||||
if div == 20:
|
||||
start = {'movable': 0, 'fixed': 8, 'dual': 4}[_modality(si)]
|
||||
return (start + pi) % 12
|
||||
if div == 27:
|
||||
start = {0: 0, 1: 3, 2: 6, 3: 9}[_element(si)]
|
||||
return (start + pi) % 12
|
||||
if div == 30:
|
||||
return _d30_map_vedastro(si, pi)
|
||||
if div == 45:
|
||||
start = {'movable': 0, 'fixed': 4, 'dual': 8}[_modality(si)]
|
||||
return (start + pi) % 12
|
||||
if div == 60:
|
||||
return (si + pi) % 12
|
||||
return varga_map(si, pi, div)
|
||||
|
||||
def calc_varga(lon, div, mode='classical_local'):
|
||||
"""计算行星在指定分盘的位置(星座+精确度数+尊贵状态)"""
|
||||
si=_si(lon); d=lon-si*30; ps=30.0/div; pi=int(d/ps)
|
||||
si=_si(lon); d=lon-si*30
|
||||
if mode == 'vedastro' and div == 2:
|
||||
ps = 10.0
|
||||
pi = int(d / ps)
|
||||
dp = (d - pi * ps) * 3
|
||||
dp_display = round(dp, 4)
|
||||
if dp_display >= 30:
|
||||
dp_display = 0.0
|
||||
vsi = varga_map_vedastro(si, pi, div)
|
||||
r={'sign':_sn(vsi),'sign_idx':vsi,'degree_in_sign':dp_display,
|
||||
'part_index':pi,'lord':SIGN_LORDS.get(_sn(vsi),'')}
|
||||
return r
|
||||
ps=30.0/div; pi=int(d/ps)
|
||||
# For all vargas, degree within divisional sign is scaled to 0-30 degrees.
|
||||
dp=(d-pi*ps)*div
|
||||
# Keep the displayed divisional degree inside [0, 30). Values such as
|
||||
@@ -84,7 +152,7 @@ def calc_varga(lon, div):
|
||||
dp_display = round(dp, 4)
|
||||
if dp_display >= 30:
|
||||
dp_display = 0.0
|
||||
vsi=varga_map(si,pi,div)
|
||||
vsi=varga_map_vedastro(si,pi,div) if mode == 'vedastro' else varga_map(si,pi,div)
|
||||
r={'sign':_sn(vsi),'sign_idx':vsi,'degree_in_sign':dp_display,
|
||||
'part_index':pi,'lord':SIGN_LORDS.get(_sn(vsi),'')}
|
||||
if div==9: r['pada']=pi+1
|
||||
@@ -173,7 +241,7 @@ def dignity(planet, sign_idx):
|
||||
if planet in OWN_SIGNS and sign_idx in OWN_SIGNS[planet]: return 'Own Sign'
|
||||
return 'Neutral'
|
||||
|
||||
def calc_all_vargas(planet_lons, asc_lon, divisions=None):
|
||||
def calc_all_vargas(planet_lons, asc_lon, divisions=None, mode='classical_local'):
|
||||
"""批量计算所有指定分盘"""
|
||||
if divisions is None:
|
||||
divisions=[2,3,4,7,9,10,12,16,20,24,27,30,40,45,60]
|
||||
@@ -183,9 +251,9 @@ def calc_all_vargas(planet_lons, asc_lon, divisions=None):
|
||||
key=f"D{div}_{m.get('name',f'D{div}')}"
|
||||
vd={'_meta':{'div':div,'name':m.get('name',''),'cn':m.get('cn',''),
|
||||
'area':m.get('area',''),'part_size':30.0/div}}
|
||||
vd['Ascendant']=calc_varga(asc_lon,div)
|
||||
vd['Ascendant']=calc_varga(asc_lon,div,mode=mode)
|
||||
for pn,lon in planet_lons.items():
|
||||
vd[pn]=calc_varga(lon,div)
|
||||
vd[pn]=calc_varga(lon,div,mode=mode)
|
||||
# 尊贵状态
|
||||
vd['_dignity']={pn:dignity(pn,pd['sign_idx'])
|
||||
for pn,pd in vd.items() if pn not in ('Ascendant','_meta') and isinstance(pd,dict) and 'sign_idx' in pd}
|
||||
|
||||
@@ -38,6 +38,45 @@ ROUTE_DOMAIN_MAP = {
|
||||
}
|
||||
|
||||
|
||||
ROUTE_THEME_REQUIREMENTS = {
|
||||
"relationship": {
|
||||
"route": "relationship",
|
||||
"requires_dual_dasha": True,
|
||||
"required_local_supplements": ["upapada_lagna", "darakaraka", "narayana_dasha", "functional_benefic_malefic"],
|
||||
},
|
||||
"marriage": {
|
||||
"route": "relationship",
|
||||
"requires_dual_dasha": True,
|
||||
"required_local_supplements": ["upapada_lagna", "darakaraka", "narayana_dasha", "functional_benefic_malefic"],
|
||||
},
|
||||
"career": {
|
||||
"route": "career",
|
||||
"requires_dual_dasha": True,
|
||||
"required_local_supplements": ["a10_karma_pada", "narayana_dasha", "functional_benefic_malefic"],
|
||||
},
|
||||
"finance": {
|
||||
"route": "finance",
|
||||
"requires_dual_dasha": True,
|
||||
"required_local_supplements": ["wealth_structure_explainer", "narayana_dasha", "functional_benefic_malefic"],
|
||||
},
|
||||
"wealth": {
|
||||
"route": "finance",
|
||||
"requires_dual_dasha": True,
|
||||
"required_local_supplements": ["wealth_structure_explainer", "narayana_dasha", "functional_benefic_malefic"],
|
||||
},
|
||||
"overview": {
|
||||
"route": "overview",
|
||||
"requires_dual_dasha": True,
|
||||
"required_local_supplements": [],
|
||||
},
|
||||
"general": {
|
||||
"route": "general",
|
||||
"requires_dual_dasha": True,
|
||||
"required_local_supplements": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _default_window(reference_date: str | None, days: int = 180) -> tuple[str, str]:
|
||||
raw = str(reference_date or datetime.utcnow().strftime("%Y-%m-%d"))[:10]
|
||||
try:
|
||||
@@ -84,6 +123,8 @@ def orchestrate_vedastro_evidence(
|
||||
domain_reports: dict[str, Any] = {}
|
||||
evidence_ledger: list[dict[str, Any]] = []
|
||||
top_events_by_domain: dict[str, Any] = {}
|
||||
daily_windows_by_domain: dict[str, list[dict[str, Any]]] = {}
|
||||
top_daily_window_by_domain: dict[str, dict[str, Any]] = {}
|
||||
domain_statuses: dict[str, Any] = {}
|
||||
domain_event_counts: dict[str, int] = {}
|
||||
available = False
|
||||
@@ -92,6 +133,24 @@ def orchestrate_vedastro_evidence(
|
||||
case,
|
||||
case_id=f"{case_id}_official_full_snapshot",
|
||||
)
|
||||
official_metadata = official_full_snapshot.get("source_metadata") if isinstance(official_full_snapshot, dict) else {}
|
||||
if not isinstance(official_metadata, dict):
|
||||
official_metadata = {}
|
||||
full_catalog = official_metadata.get("official_full_capability_catalog")
|
||||
if not isinstance(full_catalog, dict):
|
||||
full_catalog = {}
|
||||
full_catalog_domain_routing = full_catalog.get("domain_routing") if isinstance(full_catalog.get("domain_routing"), dict) else {}
|
||||
full_catalog_dynamic_selection = full_catalog.get("dynamic_selection") if isinstance(full_catalog.get("dynamic_selection"), dict) else {}
|
||||
official_section_statuses = official_full_snapshot.get("section_statuses") if isinstance(official_full_snapshot, dict) else {}
|
||||
if not isinstance(official_section_statuses, dict):
|
||||
official_section_statuses = {}
|
||||
official_report_references = {
|
||||
theme: selection.get("report_reference")
|
||||
for theme, selection in full_catalog_dynamic_selection.items()
|
||||
if isinstance(selection, dict) and isinstance(selection.get("report_reference"), dict)
|
||||
}
|
||||
theme_requirements = ROUTE_THEME_REQUIREMENTS.get(route, ROUTE_THEME_REQUIREMENTS["general"]).copy()
|
||||
theme_requirements["domains"] = list(domains)
|
||||
|
||||
for domain in domains:
|
||||
report = run_range_scan_for_case(
|
||||
@@ -108,6 +167,12 @@ def orchestrate_vedastro_evidence(
|
||||
first_reason = first_reason or report.get("reason")
|
||||
if isinstance(report.get("top_event"), dict):
|
||||
top_events_by_domain[domain] = report["top_event"]
|
||||
daily_windows = report.get("daily_windows")
|
||||
if isinstance(daily_windows, list):
|
||||
daily_windows_by_domain[domain] = daily_windows
|
||||
top_daily_window = report.get("top_daily_window")
|
||||
if isinstance(top_daily_window, dict):
|
||||
top_daily_window_by_domain[domain] = top_daily_window
|
||||
for event in report.get("evidence_ledger") or []:
|
||||
if isinstance(event, dict):
|
||||
evidence_ledger.append(event)
|
||||
@@ -123,6 +188,8 @@ def orchestrate_vedastro_evidence(
|
||||
"event_count": len(evidence_ledger),
|
||||
"top_event": next(iter(top_events_by_domain.values()), None),
|
||||
"top_events_by_domain": top_events_by_domain,
|
||||
"daily_windows_by_domain": daily_windows_by_domain,
|
||||
"top_daily_window_by_domain": top_daily_window_by_domain,
|
||||
"evidence_ledger": evidence_ledger,
|
||||
"reason": None if status == "ok" else first_reason,
|
||||
"official_full_snapshot": official_full_snapshot,
|
||||
@@ -130,8 +197,26 @@ def orchestrate_vedastro_evidence(
|
||||
"source_metadata": {
|
||||
"auto_ingested_by": "VedAstroEvidenceOrchestrator",
|
||||
"strategy": "official_full_snapshot_first_then_route_scoped_range_scan",
|
||||
"official_python_path": (
|
||||
(official_full_snapshot.get("source_metadata") or {}).get("official_python_path")
|
||||
if isinstance(official_full_snapshot, dict)
|
||||
else None
|
||||
),
|
||||
"official_python_bundle_status": (
|
||||
(official_metadata.get("official_python_bundle") or {}).get("status")
|
||||
),
|
||||
"official_full_capability_catalog_status": full_catalog.get("status"),
|
||||
"official_full_capability_catalog_summary": full_catalog.get("summary") or {},
|
||||
"official_full_capability_domain_routing": full_catalog_domain_routing,
|
||||
"official_full_capability_dynamic_selection": full_catalog_dynamic_selection,
|
||||
"official_report_references": official_report_references,
|
||||
"official_section_statuses": official_section_statuses,
|
||||
"theme_requirements": theme_requirements,
|
||||
"node_coverage": {
|
||||
"official_full_snapshot_first": True,
|
||||
"official_full_capability_catalog_default": bool(full_catalog),
|
||||
"official_full_capability_theme_routing": bool(full_catalog_domain_routing),
|
||||
"official_full_capability_dynamic_selection": bool(full_catalog_dynamic_selection),
|
||||
"strategy": "domain_scoped_range_scan",
|
||||
"official_calculation_coverage": VEDASTRO_CALCULATION_COVERAGE,
|
||||
"selected_domains": domains,
|
||||
|
||||
@@ -11,6 +11,8 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib import request
|
||||
@@ -20,17 +22,19 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_OUTPUT = ROOT / "scratch" / "local" / "vedastro_adapter" / "method_catalog_snapshot.json"
|
||||
OFFICIAL_TAG_CATALOG_URL = "https://api.vedastro.org/api/Calculate/GetAllEventDataGroupedByTag"
|
||||
STUB_ENV = "VEDASTRO_METHOD_CATALOG_STUB"
|
||||
PYTHON_BRIDGE = ROOT / "scripts" / "vedastro_python_bridge.py"
|
||||
|
||||
|
||||
def schema() -> dict[str, Any]:
|
||||
return {
|
||||
"sync": "vedastro_method_catalog_sync",
|
||||
"scope": "official_vedastro_method_catalog",
|
||||
"operations": ["sync_tags", "write_snapshot"],
|
||||
"operations": ["sync_tags", "sync_python_capabilities", "write_snapshot"],
|
||||
"sources": {
|
||||
"official_tag_catalog": OFFICIAL_TAG_CATALOG_URL,
|
||||
"official_python_package": "vedastro.Calculate",
|
||||
},
|
||||
"output_contract": ["source", "summary", "tag_groups"],
|
||||
"output_contract": ["source", "summary", "tag_groups", "python_capabilities", "python_signature_buckets"],
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +45,39 @@ def _load_stubbed_catalog() -> dict[str, Any] | None:
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
def _build_python_signature_buckets(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
buckets: dict[str, dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
bucket = str(row.get("bucket") or "unknown")
|
||||
entry = buckets.setdefault(bucket, {"count": 0, "examples": []})
|
||||
entry["count"] += 1
|
||||
if len(entry["examples"]) < 10:
|
||||
entry["examples"].append(row["method"])
|
||||
return buckets
|
||||
|
||||
|
||||
def _scan_python_capabilities() -> list[dict[str, Any]]:
|
||||
if not PYTHON_BRIDGE.exists():
|
||||
return []
|
||||
completed = subprocess.run(
|
||||
[sys.executable, str(PYTHON_BRIDGE), "--list-capabilities"],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=240,
|
||||
check=False,
|
||||
env=os.environ.copy(),
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
return []
|
||||
try:
|
||||
payload = json.loads(completed.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
capabilities = payload.get("capabilities")
|
||||
return capabilities if isinstance(capabilities, list) else []
|
||||
|
||||
|
||||
def _fetch_official_tag_catalog() -> dict[str, Any]:
|
||||
with request.urlopen(OFFICIAL_TAG_CATALOG_URL, timeout=60) as resp:
|
||||
payload = json.loads(resp.read().decode("utf-8"))
|
||||
@@ -58,9 +95,22 @@ def build_catalog() -> dict[str, Any]:
|
||||
for events in tag_groups.values():
|
||||
if isinstance(events, list):
|
||||
method_count += len(events)
|
||||
python_capabilities = catalog.get("python_capabilities")
|
||||
if not isinstance(python_capabilities, list):
|
||||
try:
|
||||
python_capabilities = _scan_python_capabilities()
|
||||
except Exception:
|
||||
python_capabilities = []
|
||||
python_signature_buckets = _build_python_signature_buckets(python_capabilities)
|
||||
python_callable_count = sum(1 for row in python_capabilities if row.get("callable"))
|
||||
catalog["python_capabilities"] = python_capabilities
|
||||
catalog["python_signature_buckets"] = python_signature_buckets
|
||||
catalog["summary"] = {
|
||||
"tag_count": len(tag_groups),
|
||||
"method_count": method_count,
|
||||
"python_capability_count": len(python_capabilities),
|
||||
"python_callable_count": python_callable_count,
|
||||
"python_signature_bucket_count": len(python_signature_buckets),
|
||||
}
|
||||
return catalog
|
||||
|
||||
|
||||
@@ -0,0 +1,820 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run selected official VedAstro Python capabilities through the shared bridge."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PYTHON = sys.executable
|
||||
BRIDGE = ROOT / "scripts" / "vedastro_python_bridge.py"
|
||||
STUB_ENV = "VEDASTRO_OFFICIAL_CAPABILITY_RUNNER_STUB"
|
||||
CATALOG_STUB_ENV = "VEDASTRO_OFFICIAL_CAPABILITY_CATALOG_STUB"
|
||||
PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu", "Ascendant"]
|
||||
HOUSES = [f"House{i}" for i in range(1, 13)]
|
||||
DEFAULT_SIGIL_SAMPLE_LIMIT = int(os.environ.get("VEDASTRO_FULL_CATALOG_SAMPLE_LIMIT", "0") or 0)
|
||||
DOMAIN_ORDER = ["career", "marriage", "wealth", "rectification", "timing", "general"]
|
||||
DEFAULT_DYNAMIC_THEMES = ["career", "marriage", "wealth", "rectification", "timing"]
|
||||
POLICY_BUCKETS = {
|
||||
"needs_user_context": "needs_user_context_methods",
|
||||
"needs_user_text": "needs_user_text_methods",
|
||||
"needs_rectification_profile": "needs_rectification_profile_methods",
|
||||
"blocked": "blocked_methods",
|
||||
}
|
||||
|
||||
|
||||
def _method_words(method: str) -> set[str]:
|
||||
return {item.lower() for item in re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)|\d+", method)}
|
||||
|
||||
|
||||
def _domain_routing_for_method(method: str, capability: dict[str, Any], parameter_strategy: str) -> dict[str, Any]:
|
||||
method_lower = method.lower()
|
||||
words = _method_words(method)
|
||||
text = " ".join(
|
||||
str(value or "")
|
||||
for value in (
|
||||
method,
|
||||
capability.get("signature"),
|
||||
capability.get("bucket"),
|
||||
" ".join(str(name) for name in capability.get("parameter_names") or []),
|
||||
)
|
||||
).lower()
|
||||
if method_lower == "getalleventdatagroupedbytag" or "groupedbytag" in text:
|
||||
return {
|
||||
"domains": ["general"],
|
||||
"execution_policy": "auto" if parameter_strategy != "unsupported_signature" else "blocked",
|
||||
"priority": "low",
|
||||
}
|
||||
domains: set[str] = set()
|
||||
priority = "low"
|
||||
is_dasha_timing = (
|
||||
"dasa" in words
|
||||
or "dasha" in words
|
||||
or method_lower.startswith(("dasaat", "dashaat", "getdasaat", "getdashaat"))
|
||||
)
|
||||
|
||||
if is_dasha_timing or any(token in text for token in ("event", "search", "timing", "transit", "gochara")):
|
||||
domains.update({"career", "marriage", "wealth", "rectification", "timing"})
|
||||
priority = "high"
|
||||
if any(token in text for token in ("marriage", "spouse", "match", "compat", "relationship", "ashtakoot", "kuta")):
|
||||
domains.add("marriage")
|
||||
priority = "high" if any(token in text for token in ("marriage", "match", "compat")) else priority
|
||||
if any(token in text for token in ("career", "profession", "job", "work", "tenth", "house10", "house 10")):
|
||||
domains.add("career")
|
||||
priority = "high"
|
||||
if any(token in text for token in ("wealth", "money", "finance", "income", "gain", "house2", "house11", "ashtakvarga")):
|
||||
domains.add("wealth")
|
||||
priority = "high" if priority == "low" else priority
|
||||
if any(token in text for token in ("birth", "rectification", "appearance", "body", "height", "shape", "complexion")):
|
||||
domains.add("rectification")
|
||||
if priority == "low":
|
||||
priority = "medium"
|
||||
if any(token in text for token in ("planet", "house", "strength", "bala", "longitude", "rasi", "navamsa", "varga")):
|
||||
domains.update({"career", "marriage", "wealth"})
|
||||
if priority == "low":
|
||||
priority = "medium"
|
||||
|
||||
if not domains:
|
||||
domains.add("general")
|
||||
|
||||
if parameter_strategy in {"requires_user_context", "requires_user_text", "requires_rectification_profile"}:
|
||||
execution_policy = {
|
||||
"requires_user_context": "needs_user_context",
|
||||
"requires_user_text": "needs_user_text",
|
||||
"requires_rectification_profile": "needs_rectification_profile",
|
||||
}[parameter_strategy]
|
||||
elif parameter_strategy == "unsupported_signature":
|
||||
execution_policy = "blocked"
|
||||
else:
|
||||
execution_policy = "auto"
|
||||
|
||||
ordered_domains = [domain for domain in DOMAIN_ORDER if domain in domains]
|
||||
return {
|
||||
"domains": ordered_domains,
|
||||
"execution_policy": execution_policy,
|
||||
"priority": priority,
|
||||
}
|
||||
|
||||
|
||||
def _build_domain_routing(method_statuses: dict[str, Any]) -> dict[str, Any]:
|
||||
routing: dict[str, dict[str, Any]] = {}
|
||||
for method, status in method_statuses.items():
|
||||
if not isinstance(status, dict):
|
||||
continue
|
||||
for domain in status.get("domains") or ["general"]:
|
||||
row = routing.setdefault(
|
||||
domain,
|
||||
{
|
||||
"method_count": 0,
|
||||
"auto_method_count": 0,
|
||||
"needs_user_context_count": 0,
|
||||
"needs_user_text_count": 0,
|
||||
"blocked_method_count": 0,
|
||||
"high_priority_methods": [],
|
||||
},
|
||||
)
|
||||
row["method_count"] += 1
|
||||
policy = status.get("execution_policy")
|
||||
if policy == "auto":
|
||||
row["auto_method_count"] += 1
|
||||
elif policy == "needs_user_context":
|
||||
row["needs_user_context_count"] += 1
|
||||
elif policy == "needs_user_text":
|
||||
row["needs_user_text_count"] += 1
|
||||
elif policy in {"blocked", "needs_rectification_profile"}:
|
||||
row["blocked_method_count"] += 1
|
||||
if policy == "auto" and status.get("priority") == "high" and method not in row["high_priority_methods"]:
|
||||
row["high_priority_methods"].append(method)
|
||||
for row in routing.values():
|
||||
row["high_priority_methods"] = row["high_priority_methods"][:24]
|
||||
return routing
|
||||
|
||||
|
||||
def _requested_dynamic_themes(payload: dict[str, Any]) -> list[str]:
|
||||
raw = payload.get("themes") or payload.get("theme") or DEFAULT_DYNAMIC_THEMES
|
||||
if isinstance(raw, str):
|
||||
values = [raw]
|
||||
elif isinstance(raw, list):
|
||||
values = raw
|
||||
else:
|
||||
values = DEFAULT_DYNAMIC_THEMES
|
||||
aliases = {
|
||||
"relationship": "marriage",
|
||||
"relationships": "marriage",
|
||||
"finance": "wealth",
|
||||
"money": "wealth",
|
||||
"birth_time": "rectification",
|
||||
"birth-time": "rectification",
|
||||
"birthtime": "rectification",
|
||||
"event": "timing",
|
||||
"events": "timing",
|
||||
"事业": "career",
|
||||
"婚恋": "marriage",
|
||||
"婚姻": "marriage",
|
||||
"财富": "wealth",
|
||||
"校时": "rectification",
|
||||
"应期": "timing",
|
||||
}
|
||||
themes: list[str] = []
|
||||
for value in values:
|
||||
key = aliases.get(str(value).strip().lower(), str(value).strip().lower())
|
||||
if key in DOMAIN_ORDER and key not in themes:
|
||||
themes.append(key)
|
||||
return themes or list(DEFAULT_DYNAMIC_THEMES)
|
||||
|
||||
|
||||
def _method_priority_score(method: str, status: dict[str, Any], theme: str) -> tuple[int, str]:
|
||||
policy = str(status.get("execution_policy") or "")
|
||||
priority = str(status.get("priority") or "low")
|
||||
score = 0
|
||||
if policy == "auto":
|
||||
score += 100
|
||||
elif policy == "needs_user_context":
|
||||
score += 60
|
||||
elif policy in {"needs_user_text", "needs_rectification_profile"}:
|
||||
score += 45
|
||||
else:
|
||||
score += 10
|
||||
if priority == "high":
|
||||
score += 40
|
||||
elif priority == "medium":
|
||||
score += 20
|
||||
if status.get("status") == "ok":
|
||||
score += 12
|
||||
if status.get("executed") is True:
|
||||
score += 8
|
||||
if theme in status.get("domains", []):
|
||||
score += 5
|
||||
method_lower = method.lower()
|
||||
if any(token in method_lower for token in ("searchevents", "eventsatrange", "eventsattime", "geteventtiming")):
|
||||
score += 10
|
||||
if theme == "career" and any(token in method_lower for token in ("dashamamsha", "profession", "career", "tenth")):
|
||||
score += 8
|
||||
if theme == "marriage" and any(token in method_lower for token in ("match", "marriage", "spouse", "ashtakoot")):
|
||||
score += 8
|
||||
if theme == "wealth" and any(token in method_lower for token in ("wealth", "money", "income", "gain", "ashtakvarga")):
|
||||
score += 8
|
||||
if theme == "rectification" and any(token in method_lower for token in ("birth", "appearance", "body")):
|
||||
score += 8
|
||||
if theme == "timing" and any(token in method_lower for token in ("dasa", "dasha", "event", "transit")):
|
||||
score += 8
|
||||
return (-score, method)
|
||||
|
||||
|
||||
def _capability_reference(method: str, status: dict[str, Any], theme: str) -> dict[str, Any]:
|
||||
return {
|
||||
"citation_id": f"vedastro:{theme}:{method}",
|
||||
"method": method,
|
||||
"status": status.get("status"),
|
||||
"execution_policy": status.get("execution_policy"),
|
||||
"priority": status.get("priority"),
|
||||
"domains": status.get("domains") or [],
|
||||
"bucket": status.get("bucket"),
|
||||
"signature": status.get("signature"),
|
||||
"parameter_names": status.get("parameter_names") or [],
|
||||
"executed": bool(status.get("executed")),
|
||||
"source": "official_full_capability_catalog",
|
||||
}
|
||||
|
||||
|
||||
def _build_dynamic_selection(
|
||||
method_statuses: dict[str, Any],
|
||||
domain_routing: dict[str, Any],
|
||||
requested_themes: list[str],
|
||||
*,
|
||||
limit: int = 12,
|
||||
) -> dict[str, Any]:
|
||||
selection: dict[str, Any] = {}
|
||||
for theme in requested_themes:
|
||||
candidates = [
|
||||
(method, status)
|
||||
for method, status in method_statuses.items()
|
||||
if isinstance(status, dict) and theme in (status.get("domains") or [])
|
||||
]
|
||||
candidates.sort(key=lambda item: _method_priority_score(item[0], item[1], theme))
|
||||
selected = [
|
||||
_capability_reference(method, status, theme)
|
||||
for method, status in candidates
|
||||
if status.get("execution_policy") == "auto"
|
||||
][:limit]
|
||||
policy_groups: dict[str, list[dict[str, Any]]] = {
|
||||
"needs_user_context_methods": [],
|
||||
"needs_user_text_methods": [],
|
||||
"needs_rectification_profile_methods": [],
|
||||
"blocked_methods": [],
|
||||
}
|
||||
for method, status in candidates:
|
||||
bucket_name = POLICY_BUCKETS.get(str(status.get("execution_policy") or ""))
|
||||
if bucket_name and len(policy_groups[bucket_name]) < limit:
|
||||
policy_groups[bucket_name].append(_capability_reference(method, status, theme))
|
||||
citation_ids = [item["citation_id"] for item in selected]
|
||||
selection[theme] = {
|
||||
"requested_theme": theme,
|
||||
"selection_policy": "official_catalog_theme_top_n",
|
||||
"domain_summary": domain_routing.get(theme) or {},
|
||||
"selected_methods": selected,
|
||||
**policy_groups,
|
||||
"report_reference": {
|
||||
"theme": theme,
|
||||
"citation_ids": citation_ids,
|
||||
"auto_count": len(selected),
|
||||
"needs_user_context_count": len(policy_groups["needs_user_context_methods"]),
|
||||
"needs_user_text_count": len(policy_groups["needs_user_text_methods"]),
|
||||
"needs_rectification_profile_count": len(policy_groups["needs_rectification_profile_methods"]),
|
||||
"blocked_count": len(policy_groups["blocked_methods"]),
|
||||
"boundary": "Citations identify official VedAstro capability evidence used or requested by the workflow; skipped or context-dependent methods are not treated as executed evidence.",
|
||||
},
|
||||
}
|
||||
return selection
|
||||
|
||||
|
||||
def schema() -> dict[str, Any]:
|
||||
return {
|
||||
"runner": "vedastro_official_capability_runner",
|
||||
"primary_source": "vedastro_python_bridge",
|
||||
"operations": ["run_bucket", "run_selected_methods", "run_snapshot_bundle", "run_full_capability_catalog"],
|
||||
"request_contract": ["methods_json", "birth_json", "bundle?"],
|
||||
"response_contract": ["summary", "results", "result?"],
|
||||
}
|
||||
|
||||
|
||||
def _normalize_tz(value: Any) -> str:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if value is None:
|
||||
return "+00:00"
|
||||
sign = "+" if float(value) >= 0 else "-"
|
||||
absolute = abs(float(value))
|
||||
hours = int(absolute)
|
||||
minutes = int(round((absolute - hours) * 60))
|
||||
return f"{sign}{hours:02d}:{minutes:02d}"
|
||||
|
||||
|
||||
def _bridge_time(case: dict[str, Any], date_text: str, *, hour: int | None = None, minute: int | None = None) -> dict[str, Any]:
|
||||
year, month, day = str(date_text).split("-")
|
||||
return {
|
||||
"__vedastro_type__": "Time",
|
||||
"year": int(year),
|
||||
"month": int(month),
|
||||
"day": int(day),
|
||||
"hour": int(case.get("hour", 0) if hour is None else hour),
|
||||
"minute": int(case.get("minute", 0) if minute is None else minute),
|
||||
"offset": _normalize_tz(case.get("tz")),
|
||||
"geolocation": {
|
||||
"__vedastro_type__": "GeoLocation",
|
||||
"location_name": "UserLocation",
|
||||
"longitude": case.get("lon"),
|
||||
"latitude": case.get("lat"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _reference_date(case: dict[str, Any]) -> str:
|
||||
for key in ("reference_date", "today", "transit_date", "current_date"):
|
||||
value = case.get(key)
|
||||
if value:
|
||||
return str(value)[:10]
|
||||
return datetime.utcnow().strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _build_method_payload(method: str, case: dict[str, Any]) -> dict[str, Any] | None:
|
||||
birth_date = f"{int(case['year']):04d}-{int(case['month']):02d}-{int(case['day']):02d}"
|
||||
birth_time = _bridge_time(case, birth_date)
|
||||
ref_date = _reference_date(case)
|
||||
check_time = _bridge_time(case, ref_date)
|
||||
start_of_year = f"{ref_date[:4]}-01-01"
|
||||
end_of_year = f"{ref_date[:4]}-12-31"
|
||||
|
||||
if method == "GetAllEventDataGroupedByTag":
|
||||
return {}
|
||||
if method == "DasaAtTime":
|
||||
return {"args": [birth_time, check_time, 3]}
|
||||
if method == "GetCharaDasaAtTime":
|
||||
return {"args": [birth_time, check_time]}
|
||||
if method == "DasaAtRange":
|
||||
return {"args": [birth_time, _bridge_time(case, start_of_year, hour=0, minute=0), _bridge_time(case, end_of_year, hour=23, minute=59), 3, 100]}
|
||||
if method in {"AllPlanetStrength", "AshtakvargaLifeMap"}:
|
||||
return {"args": [birth_time]}
|
||||
if method == "AllPlanetData":
|
||||
return {"args": [{"__vedastro_enum__": "PlanetName", "value": "Sun"}, birth_time]}
|
||||
if method == "AllHouseData":
|
||||
return {"args": [{"__vedastro_enum__": "HouseName", "value": "House1"}, birth_time]}
|
||||
return None
|
||||
|
||||
|
||||
def _method_payload_for_instance(method: str, case: dict[str, Any], identity: str | None = None) -> dict[str, Any] | None:
|
||||
payload = _build_method_payload(method, case)
|
||||
if payload is None:
|
||||
return None
|
||||
if method == "AllPlanetData" and identity:
|
||||
return {"args": [{"__vedastro_enum__": "PlanetName", "value": str(identity)}, payload["args"][1]]}
|
||||
if method == "AllHouseData" and identity:
|
||||
return {"args": [{"__vedastro_enum__": "HouseName", "value": str(identity)}, payload["args"][1]]}
|
||||
return payload
|
||||
|
||||
|
||||
def _call_bridge(method: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
[PYTHON, str(BRIDGE), "--method", method, "--params-json", json.dumps(payload, ensure_ascii=False)],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=240,
|
||||
check=False,
|
||||
env=os.environ.copy(),
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
return {"available": False, "status": "bridge_runtime_error", "stderr": (completed.stderr or "").strip()}
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def _list_official_capabilities() -> dict[str, Any]:
|
||||
stub_raw = os.environ.get(CATALOG_STUB_ENV, "").strip()
|
||||
if stub_raw:
|
||||
payload = json.loads(stub_raw)
|
||||
payload.setdefault("source", "stubbed_official_capability_catalog")
|
||||
return payload
|
||||
|
||||
completed = subprocess.run(
|
||||
[PYTHON, str(BRIDGE), "--list-capabilities"],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=240,
|
||||
check=False,
|
||||
env=os.environ.copy(),
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
return {
|
||||
"available": False,
|
||||
"status": "bridge_runtime_error",
|
||||
"capabilities": [],
|
||||
"buckets": {},
|
||||
"stderr": (completed.stderr or "").strip(),
|
||||
"source": "vedastro_official_capability_runner",
|
||||
}
|
||||
try:
|
||||
return json.loads(completed.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"available": False,
|
||||
"status": "bridge_invalid_json",
|
||||
"capabilities": [],
|
||||
"buckets": {},
|
||||
"stdout_excerpt": (completed.stdout or "").strip()[:500],
|
||||
"source": "vedastro_official_capability_runner",
|
||||
}
|
||||
|
||||
|
||||
def run_selected_methods(methods: list[str], birth_payload: dict[str, Any]) -> dict[str, Any]:
|
||||
stub_raw = os.environ.get(STUB_ENV, "").strip()
|
||||
stub_map = json.loads(stub_raw) if stub_raw else {}
|
||||
results: dict[str, Any] = {}
|
||||
ok_count = 0
|
||||
skipped_count = 0
|
||||
for method in methods:
|
||||
if method in stub_map:
|
||||
results[method] = stub_map[method]
|
||||
else:
|
||||
payload = _build_method_payload(method, birth_payload)
|
||||
if payload is None:
|
||||
results[method] = {"available": False, "status": "unsupported_signature"}
|
||||
skipped_count += 1
|
||||
continue
|
||||
results[method] = _call_bridge(method, payload)
|
||||
if results[method].get("status") == "ok":
|
||||
ok_count += 1
|
||||
|
||||
return {
|
||||
"runner": "vedastro_official_capability_runner",
|
||||
"primary_source": "vedastro_python_bridge",
|
||||
"summary": {
|
||||
"requested_method_count": len(methods),
|
||||
"executed_method_count": len(results),
|
||||
"ok_count": ok_count,
|
||||
"skipped_count": skipped_count,
|
||||
},
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
def _sample_identity_for_method(method: str, parameter_names: list[str]) -> Any:
|
||||
lowered = [name.lower() for name in parameter_names]
|
||||
if "planetname" in lowered or "inputplanet" in lowered or "planet" in lowered:
|
||||
return "Sun"
|
||||
if "housename" in lowered:
|
||||
return "House1"
|
||||
if "housenumber" in lowered or "inputhousenumber" in lowered:
|
||||
return 1
|
||||
if "inputhouse" in lowered or "house" in lowered:
|
||||
return "House1"
|
||||
if "zodiacname" in lowered or "signname" in lowered or "inputsign" in lowered or "zodiacsign" in lowered:
|
||||
return "Aries"
|
||||
if "constellation" in lowered:
|
||||
return "Aswini"
|
||||
if "divisionalno" in lowered:
|
||||
return 9
|
||||
if "longitude" in lowered or "longitudedeg" in lowered or "totaldegrees" in lowered:
|
||||
return 3.5
|
||||
return None
|
||||
|
||||
|
||||
def _full_catalog_method_payload(method: str, capability: dict[str, Any], case: dict[str, Any]) -> tuple[dict[str, Any] | None, str]:
|
||||
parameter_names = [str(name) for name in capability.get("parameter_names") or []]
|
||||
lowered = [name.lower() for name in parameter_names]
|
||||
bucket = str(capability.get("bucket") or "")
|
||||
birth_date = f"{int(case['year']):04d}-{int(case['month']):02d}-{int(case['day']):02d}"
|
||||
birth_time = _bridge_time(case, birth_date)
|
||||
ref_date = _reference_date(case)
|
||||
check_time = _bridge_time(case, ref_date)
|
||||
start_of_year = _bridge_time(case, f"{ref_date[:4]}-01-01", hour=0, minute=0)
|
||||
end_of_year = _bridge_time(case, f"{ref_date[:4]}-12-31", hour=23, minute=59)
|
||||
|
||||
if not parameter_names:
|
||||
return {}, "auto_zero_arg"
|
||||
if bucket in {"time_only", "(inputTime)", "(queryTime)", "(time1)"} or lowered in (["time"], ["inputtime"], ["querytime"], ["time1"]):
|
||||
key = parameter_names[0]
|
||||
return {"kwargs": {key: check_time}}, "auto_time_only"
|
||||
if bucket == "birth_time_only" or lowered == ["birthtime"]:
|
||||
return {"kwargs": {parameter_names[0]: birth_time}}, "auto_birth_time_only"
|
||||
if bucket == "planet_time" or lowered == ["planetname", "time"]:
|
||||
return {"args": [{"__vedastro_enum__": "PlanetName", "value": "Sun"}, birth_time]}, "auto_planet_time"
|
||||
if bucket == "planet_alias_time" or lowered == ["planet", "time"]:
|
||||
return {"args": [{"__vedastro_enum__": "PlanetName", "value": "Sun"}, birth_time]}, "auto_planet_time"
|
||||
if bucket == "house_name_time" or lowered == ["housename", "time"]:
|
||||
return {"args": [{"__vedastro_enum__": "HouseName", "value": "House1"}, birth_time]}, "auto_house_name_time"
|
||||
if bucket == "house_number_time" or lowered == ["housenumber", "time"]:
|
||||
return {"args": [1, birth_time]}, "auto_house_number_time"
|
||||
if bucket == "dasha_at_time" or lowered == ["birthtime", "checktime", "levels"]:
|
||||
return {"args": [birth_time, check_time, 3]}, "auto_dasha_at_time"
|
||||
if bucket == "dasha_at_range" or lowered == ["birthtime", "starttime", "endtime", "levels", "precisionhours"]:
|
||||
return {"args": [birth_time, start_of_year, end_of_year, 3, 100]}, "auto_dasha_at_range"
|
||||
|
||||
if lowered == ["birthtime", "checktime"]:
|
||||
return {"args": [birth_time, check_time]}, "auto_birth_check_time"
|
||||
if lowered == ["birthtime", "levels"]:
|
||||
return {"args": [birth_time, 3]}, "auto_birth_levels"
|
||||
if lowered == ["birthtime", "scanyear"]:
|
||||
return {"args": [birth_time, int(ref_date[:4])]}, "auto_birth_scan_year"
|
||||
if lowered == ["birthtime", "querytime"]:
|
||||
return {"args": [birth_time, check_time]}, "auto_birth_query_time"
|
||||
if lowered == ["birthtime", "sortbyweight"]:
|
||||
return {"args": [birth_time, False]}, "auto_birth_bool"
|
||||
if lowered == ["birthtime", "filtertags", "sortbyweight"]:
|
||||
return {"args": [birth_time, [], False]}, "auto_birth_filter_tags"
|
||||
if lowered == ["birthtime", "starttime", "endtime", "eventtaglist", "precisionhours"]:
|
||||
return {"args": [birth_time, start_of_year, end_of_year, ["Marriage"], 100]}, "auto_events_range"
|
||||
if lowered == ["birthtime", "checktime", "eventtaglist"]:
|
||||
return {"args": [birth_time, check_time, ["Marriage"]]}, "auto_events_time"
|
||||
if lowered == ["birthtime", "attime", "eventtaglist"]:
|
||||
return {"args": [birth_time, check_time, ["Marriage"]]}, "auto_search_events"
|
||||
|
||||
if len(parameter_names) == 1:
|
||||
sample = _sample_identity_for_method(method, parameter_names)
|
||||
if sample is not None:
|
||||
return {"args": [sample]}, "auto_single_sample"
|
||||
if len(parameter_names) == 2 and any(name in lowered for name in ("time", "inputtime", "birthtime")):
|
||||
sample = _sample_identity_for_method(method, parameter_names)
|
||||
if sample is not None:
|
||||
args = []
|
||||
for name in lowered:
|
||||
if name in {"time", "inputtime", "birthtime"}:
|
||||
args.append(birth_time)
|
||||
else:
|
||||
args.append(sample)
|
||||
return {"args": args}, "auto_two_arg_sample"
|
||||
|
||||
if any(name in lowered for name in ("malebirthtime", "femalebirthtime", "partnerbirthtime", "personb", "personbirthtime")):
|
||||
return None, "requires_user_context"
|
||||
if any(name in lowered for name in ("bodyheight", "bodyshape", "hair", "lips", "nose", "complexion", "faceshape", "constitution", "personality")):
|
||||
return None, "requires_rectification_profile"
|
||||
if any(name in lowered for name in ("rawtextdata", "birthdatarawtext", "inputtext", "textinput", "query", "fullname", "personfullname", "address", "locationname", "ipaddress")):
|
||||
return None, "requires_user_text"
|
||||
return None, "unsupported_signature"
|
||||
|
||||
|
||||
def run_full_capability_catalog(birth_payload: dict[str, Any]) -> dict[str, Any]:
|
||||
catalog = _list_official_capabilities()
|
||||
capabilities = catalog.get("capabilities") if isinstance(catalog.get("capabilities"), list) else []
|
||||
buckets = catalog.get("buckets") if isinstance(catalog.get("buckets"), dict) else {}
|
||||
stub_raw = os.environ.get(STUB_ENV, "").strip()
|
||||
stub_map = json.loads(stub_raw) if stub_raw else {}
|
||||
method_statuses: dict[str, Any] = {}
|
||||
bucket_statuses: dict[str, dict[str, int]] = {}
|
||||
executed_count = 0
|
||||
ok_count = 0
|
||||
unsupported_count = 0
|
||||
blocked_count = 0
|
||||
sample_limit = max(0, int(os.environ.get("VEDASTRO_FULL_CATALOG_SAMPLE_LIMIT", str(DEFAULT_SIGIL_SAMPLE_LIMIT)) or 0))
|
||||
|
||||
for capability in capabilities:
|
||||
method = str(capability.get("method") or "")
|
||||
if not method:
|
||||
continue
|
||||
bucket = str(capability.get("bucket") or "unknown")
|
||||
payload, strategy = _full_catalog_method_payload(method, capability, birth_payload)
|
||||
routing_meta = _domain_routing_for_method(method, capability, strategy)
|
||||
bucket_row = bucket_statuses.setdefault(bucket, {"total": 0, "executed": 0, "ok": 0, "unsupported": 0, "blocked": 0})
|
||||
bucket_row["total"] += 1
|
||||
|
||||
if payload is None:
|
||||
unsupported_count += 1
|
||||
bucket_row["unsupported"] += 1
|
||||
method_statuses[method] = {
|
||||
"status": strategy,
|
||||
"bucket": bucket,
|
||||
"signature": capability.get("signature"),
|
||||
"parameter_names": capability.get("parameter_names") or [],
|
||||
"executed": False,
|
||||
**routing_meta,
|
||||
}
|
||||
continue
|
||||
|
||||
if executed_count >= sample_limit and method not in stub_map:
|
||||
blocked_count += 1
|
||||
bucket_row["blocked"] += 1
|
||||
method_statuses[method] = {
|
||||
"status": "skipped_by_sample_limit",
|
||||
"bucket": bucket,
|
||||
"signature": capability.get("signature"),
|
||||
"parameter_names": capability.get("parameter_names") or [],
|
||||
"executed": False,
|
||||
"parameter_strategy": strategy,
|
||||
**routing_meta,
|
||||
}
|
||||
continue
|
||||
|
||||
if method in stub_map:
|
||||
report = stub_map[method]
|
||||
else:
|
||||
report = _call_bridge(method, payload)
|
||||
status = str(report.get("status") or "blocked")
|
||||
executed_count += 1
|
||||
bucket_row["executed"] += 1
|
||||
if status == "ok":
|
||||
ok_count += 1
|
||||
bucket_row["ok"] += 1
|
||||
else:
|
||||
blocked_count += 1
|
||||
bucket_row["blocked"] += 1
|
||||
method_statuses[method] = {
|
||||
"status": status,
|
||||
"bucket": bucket,
|
||||
"signature": capability.get("signature"),
|
||||
"parameter_names": capability.get("parameter_names") or [],
|
||||
"executed": True,
|
||||
"parameter_strategy": strategy,
|
||||
"available": bool(report.get("available")),
|
||||
"source": report.get("source"),
|
||||
**routing_meta,
|
||||
}
|
||||
|
||||
overall_status = "blocked"
|
||||
if capabilities:
|
||||
overall_status = "ok" if unsupported_count == 0 and blocked_count == 0 else "partial"
|
||||
domain_routing = _build_domain_routing(method_statuses)
|
||||
requested_themes = _requested_dynamic_themes(birth_payload)
|
||||
dynamic_selection = _build_dynamic_selection(method_statuses, domain_routing, requested_themes)
|
||||
|
||||
return {
|
||||
"runner": "vedastro_official_capability_runner",
|
||||
"primary_source": "vedastro_python_bridge",
|
||||
"bundle": "official_full_capability_catalog",
|
||||
"available": bool(capabilities),
|
||||
"status": overall_status,
|
||||
"summary": {
|
||||
"catalog_method_count": len(capabilities),
|
||||
"official_callable_count": sum(1 for item in capabilities if item.get("callable")),
|
||||
"signature_bucket_count": len(buckets),
|
||||
"executed_method_count": executed_count,
|
||||
"ok_method_count": ok_count,
|
||||
"unsupported_method_count": unsupported_count,
|
||||
"blocked_method_count": blocked_count,
|
||||
"sample_limit": sample_limit,
|
||||
"domain_routing_count": len(domain_routing),
|
||||
"dynamic_selection_theme_count": len(dynamic_selection),
|
||||
},
|
||||
"coverage": {
|
||||
"source_mode": "official_full_capability_catalog",
|
||||
"catalog_source": catalog.get("source") or "vedastro_python_bridge",
|
||||
"python_bin": catalog.get("python_bin"),
|
||||
"bucket_count": len(buckets),
|
||||
"safe_sampling": True,
|
||||
"not_user_exposed": True,
|
||||
"lightweight_domain_mapping": True,
|
||||
"dynamic_theme_selection": True,
|
||||
},
|
||||
"domain_routing": domain_routing,
|
||||
"dynamic_selection": dynamic_selection,
|
||||
"bucket_statuses": bucket_statuses,
|
||||
"method_statuses": method_statuses,
|
||||
}
|
||||
|
||||
|
||||
def run_snapshot_bundle(bundle: str, birth_payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if bundle == "official_full_capability_catalog":
|
||||
return run_full_capability_catalog(birth_payload)
|
||||
|
||||
if bundle != "official_full_snapshot":
|
||||
return {
|
||||
"runner": "vedastro_official_capability_runner",
|
||||
"bundle": bundle,
|
||||
"available": False,
|
||||
"status": "unsupported_bundle",
|
||||
"reason": f"Unsupported bundle: {bundle}",
|
||||
}
|
||||
|
||||
stub_raw = os.environ.get(STUB_ENV, "").strip()
|
||||
stub_map = json.loads(stub_raw) if stub_raw else {}
|
||||
stub_mode = bool(stub_raw)
|
||||
snapshot_sections: dict[str, Any] = {}
|
||||
section_statuses: dict[str, str] = {}
|
||||
coverage_sections: list[str] = []
|
||||
ok_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
chart_core: dict[str, Any] = {}
|
||||
planet_statuses: dict[str, str] = {}
|
||||
for planet in PLANETS:
|
||||
stub_key = f"AllPlanetData:{planet}"
|
||||
if stub_key in stub_map:
|
||||
report = stub_map[stub_key]
|
||||
elif stub_mode:
|
||||
report = {"available": False, "status": "stub_not_provided"}
|
||||
else:
|
||||
payload = _method_payload_for_instance("AllPlanetData", birth_payload, planet)
|
||||
report = {"available": False, "status": "unsupported_signature"} if payload is None else _call_bridge("AllPlanetData", payload)
|
||||
status = str(report.get("status") or "blocked")
|
||||
planet_statuses[planet] = "ok" if status == "ok" else status
|
||||
if status == "ok":
|
||||
ok_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
chart_core[planet] = {
|
||||
"Status": "Pass" if status == "ok" else "Fail",
|
||||
"Payload": {"AllPlanetData": report.get("result")} if status == "ok" else report,
|
||||
}
|
||||
if chart_core:
|
||||
snapshot_sections["chart_core"] = chart_core
|
||||
section_statuses["chart_core"] = "ok" if all(value == "ok" for value in planet_statuses.values()) else "partial"
|
||||
section_statuses["chart_core_fanout"] = planet_statuses
|
||||
if section_statuses["chart_core"] == "ok":
|
||||
coverage_sections.append("chart_core")
|
||||
|
||||
house_core: dict[str, Any] = {}
|
||||
house_statuses: dict[str, str] = {}
|
||||
for house in HOUSES:
|
||||
stub_key = f"AllHouseData:{house}"
|
||||
if stub_key in stub_map:
|
||||
report = stub_map[stub_key]
|
||||
elif stub_mode:
|
||||
report = {"available": False, "status": "stub_not_provided"}
|
||||
else:
|
||||
payload = _method_payload_for_instance("AllHouseData", birth_payload, house)
|
||||
report = {"available": False, "status": "unsupported_signature"} if payload is None else _call_bridge("AllHouseData", payload)
|
||||
status = str(report.get("status") or "blocked")
|
||||
house_statuses[house] = "ok" if status == "ok" else status
|
||||
if status == "ok":
|
||||
ok_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
house_core[house] = {
|
||||
"Status": "Pass" if status == "ok" else "Fail",
|
||||
"Payload": {"AllHouseData": report.get("result")} if status == "ok" else report,
|
||||
}
|
||||
if house_core:
|
||||
snapshot_sections["house_core"] = house_core
|
||||
section_statuses["house_core"] = "ok" if all(value == "ok" for value in house_statuses.values()) else "partial"
|
||||
section_statuses["house_core_fanout"] = house_statuses
|
||||
if section_statuses["house_core"] == "ok":
|
||||
coverage_sections.append("house_core")
|
||||
|
||||
scalar_methods = [
|
||||
("dasha_all", "DasaAtRange", "DasaAtRange"),
|
||||
("vimshottari_now", "DasaAtTime", "DasaAtTime"),
|
||||
("chara_dasha_now", "GetCharaDasaAtTime", "GetCharaDasaAtTime"),
|
||||
("shadbala", "AllPlanetStrength", "AllPlanetStrength"),
|
||||
("ashtakavarga", "AshtakvargaLifeMap", "AshtakvargaLifeMap"),
|
||||
]
|
||||
for section_name, method, payload_key in scalar_methods:
|
||||
if method in stub_map:
|
||||
report = stub_map[method]
|
||||
elif stub_mode:
|
||||
report = {"available": False, "status": "stub_not_provided"}
|
||||
else:
|
||||
payload = _method_payload_for_instance(method, birth_payload)
|
||||
report = {"available": False, "status": "unsupported_signature"} if payload is None else _call_bridge(method, payload)
|
||||
status = str(report.get("status") or "blocked")
|
||||
section_statuses[section_name] = "ok" if status == "ok" else status
|
||||
if status == "ok":
|
||||
ok_count += 1
|
||||
coverage_sections.append(section_name)
|
||||
else:
|
||||
skipped_count += 1
|
||||
snapshot_sections[section_name] = {
|
||||
"Status": "Pass" if status == "ok" else "Fail",
|
||||
"Payload": {payload_key: report.get("result")} if status == "ok" else report,
|
||||
}
|
||||
|
||||
overall_status = "blocked"
|
||||
if coverage_sections:
|
||||
overall_status = "ok" if len(coverage_sections) == 7 else "partial"
|
||||
|
||||
return {
|
||||
"runner": "vedastro_official_capability_runner",
|
||||
"primary_source": "vedastro_python_bridge",
|
||||
"bundle": bundle,
|
||||
"available": bool(coverage_sections),
|
||||
"status": overall_status,
|
||||
"summary": {
|
||||
"requested_method_count": len(PLANETS) + len(HOUSES) + len(scalar_methods),
|
||||
"executed_method_count": len(PLANETS) + len(HOUSES) + len(scalar_methods),
|
||||
"ok_count": ok_count,
|
||||
"skipped_count": skipped_count,
|
||||
},
|
||||
"result": {
|
||||
"snapshot_sections": snapshot_sections,
|
||||
"section_statuses": section_statuses,
|
||||
"coverage": {
|
||||
"source_mode": "official_capability_runner_bundle",
|
||||
"filled_sections": coverage_sections,
|
||||
"planet_count": len(chart_core),
|
||||
"house_count": len(house_core),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--print-schema", action="store_true")
|
||||
parser.add_argument("--methods-json", default="[]")
|
||||
parser.add_argument("--birth-json", default="{}")
|
||||
parser.add_argument("--bundle", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.print_schema:
|
||||
result = schema()
|
||||
elif args.bundle:
|
||||
birth_payload = json.loads(args.birth_json or "{}")
|
||||
result = run_snapshot_bundle(args.bundle, birth_payload)
|
||||
else:
|
||||
methods = json.loads(args.methods_json or "[]")
|
||||
birth_payload = json.loads(args.birth_json or "{}")
|
||||
result = run_selected_methods(methods, birth_payload)
|
||||
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -14,6 +14,7 @@ separate. It stays deliberately thin:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import inspect
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
@@ -228,6 +229,116 @@ if __name__ == "__main__":
|
||||
"""
|
||||
|
||||
|
||||
CAPABILITY_RUNNER = r"""
|
||||
import contextlib
|
||||
import importlib
|
||||
import inspect
|
||||
import io
|
||||
import json
|
||||
|
||||
MODULE_CANDIDATES = ("vedastro", "VedAstro")
|
||||
|
||||
|
||||
def _import_module():
|
||||
for name in MODULE_CANDIDATES:
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
try:
|
||||
return name, importlib.import_module(name)
|
||||
except ModuleNotFoundError:
|
||||
continue
|
||||
return None, None
|
||||
|
||||
|
||||
def _bucket(parameter_names, signature_text):
|
||||
lowered = [name.lower() for name in parameter_names]
|
||||
if not lowered:
|
||||
return "zero_arg"
|
||||
if lowered == ["time"]:
|
||||
return "time_only"
|
||||
if lowered == ["birthtime"]:
|
||||
return "birth_time_only"
|
||||
if lowered == ["planetname", "time"]:
|
||||
return "planet_time"
|
||||
if lowered == ["housename", "time"]:
|
||||
return "house_name_time"
|
||||
if lowered == ["housenumber", "time"]:
|
||||
return "house_number_time"
|
||||
if lowered == ["planet", "time"]:
|
||||
return "planet_alias_time"
|
||||
if lowered == ["birthtime", "checktime", "levels"]:
|
||||
return "dasha_at_time"
|
||||
if lowered == ["birthtime", "starttime", "endtime", "levels", "precisionhours"]:
|
||||
return "dasha_at_range"
|
||||
return signature_text.strip() or "unknown_signature"
|
||||
|
||||
|
||||
def main():
|
||||
module_name, module = _import_module()
|
||||
if module is None:
|
||||
print(json.dumps({
|
||||
"available": False,
|
||||
"status": "python_package_not_installed",
|
||||
"source": "vedastro_python_bridge_child",
|
||||
}))
|
||||
return
|
||||
|
||||
calculate = getattr(module, "Calculate", None)
|
||||
if calculate is None:
|
||||
print(json.dumps({
|
||||
"available": False,
|
||||
"status": "calculate_namespace_missing",
|
||||
"module_name": module_name,
|
||||
"source": "vedastro_python_bridge_child",
|
||||
}))
|
||||
return
|
||||
|
||||
capabilities = []
|
||||
buckets = {}
|
||||
for name in sorted(dir(calculate)):
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
obj = getattr(calculate, name)
|
||||
if not callable(obj):
|
||||
continue
|
||||
try:
|
||||
sig = inspect.signature(obj)
|
||||
signature_text = str(sig)
|
||||
parameter_names = [param.name for param in sig.parameters.values()]
|
||||
except Exception:
|
||||
signature_text = "<unknown>"
|
||||
parameter_names = []
|
||||
bucket = _bucket(parameter_names, signature_text)
|
||||
capabilities.append({
|
||||
"method": name,
|
||||
"signature": signature_text,
|
||||
"bucket": bucket,
|
||||
"parameter_names": parameter_names,
|
||||
"callable": True,
|
||||
})
|
||||
entry = buckets.setdefault(bucket, {"count": 0, "examples": []})
|
||||
entry["count"] += 1
|
||||
if len(entry["examples"]) < 10:
|
||||
entry["examples"].append(name)
|
||||
|
||||
print(json.dumps({
|
||||
"available": True,
|
||||
"status": "ok",
|
||||
"module_name": module_name,
|
||||
"capabilities": capabilities,
|
||||
"summary": {
|
||||
"total_callable": len(capabilities),
|
||||
"signature_bucket_count": len(buckets),
|
||||
},
|
||||
"buckets": buckets,
|
||||
"source": "vedastro_python_bridge",
|
||||
}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
|
||||
|
||||
def _package_available() -> bool:
|
||||
if os.environ.get(FORCE_UNAVAILABLE_ENV, "").strip().lower() in {"1", "true", "yes"}:
|
||||
return False
|
||||
@@ -327,13 +438,44 @@ def _call_via_child_python(python_bin: str, method: str, params: dict[str, Any])
|
||||
return payload
|
||||
|
||||
|
||||
def _list_capabilities_via_child_python(python_bin: str) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
[python_bin, "-c", CAPABILITY_RUNNER],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=240,
|
||||
env=os.environ.copy(),
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
return {
|
||||
"available": False,
|
||||
"status": "bridge_runtime_error",
|
||||
"stderr": (completed.stderr or "").strip(),
|
||||
"stdout_excerpt": (completed.stdout or "").strip()[:500],
|
||||
"source": "vedastro_python_bridge",
|
||||
}
|
||||
try:
|
||||
payload = json.loads(completed.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"available": False,
|
||||
"status": "bridge_invalid_json",
|
||||
"stdout_excerpt": (completed.stdout or "").strip()[:500],
|
||||
"stderr": (completed.stderr or "").strip()[:500],
|
||||
"source": "vedastro_python_bridge",
|
||||
}
|
||||
payload["python_bin"] = python_bin
|
||||
return payload
|
||||
|
||||
|
||||
def schema() -> dict[str, Any]:
|
||||
return {
|
||||
"bridge": "vedastro_python_bridge",
|
||||
"package_name": PACKAGE_NAME,
|
||||
"module_candidates": list(MODULE_CANDIDATES),
|
||||
"intended_role": "python_sdk_bulk_calculation_bridge",
|
||||
"operations": ["call_method"],
|
||||
"operations": ["call_method", "list_capabilities"],
|
||||
"request_contract": ["method", "params_json"],
|
||||
"typed_param_contract": {
|
||||
"enum": {"__vedastro_enum__": "PlanetName", "value": "Sun"},
|
||||
@@ -401,6 +543,15 @@ def call_method(method: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return _call_via_child_python(python_bin, method, params)
|
||||
|
||||
|
||||
def list_capabilities() -> dict[str, Any]:
|
||||
if os.environ.get(FORCE_UNAVAILABLE_ENV, "").strip().lower() in {"1", "true", "yes"}:
|
||||
return _missing_package_result("list_capabilities")
|
||||
python_bin = _select_python_bin()
|
||||
if not python_bin:
|
||||
return _missing_package_result("list_capabilities")
|
||||
return _list_capabilities_via_child_python(python_bin)
|
||||
|
||||
|
||||
def call_high_value(method_key: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if method_key == "event_tag_catalog":
|
||||
return call_method("GetAllEventDataGroupedByTag", {})
|
||||
@@ -429,6 +580,119 @@ def call_high_value(method_key: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
check_time = payload["check_time"]
|
||||
return call_method("GetCharaDasaAtTime", {"args": [birth_time, check_time]})
|
||||
|
||||
if method_key == "official_full_snapshot_bundle":
|
||||
birth_time = payload["birth_time"]
|
||||
check_time = payload["check_time"]
|
||||
start_time = payload["start_time"]
|
||||
end_time = payload["end_time"]
|
||||
levels = int(payload.get("levels", 3))
|
||||
precision_hours = int(payload.get("precision_hours", 100))
|
||||
planets = list(payload.get("planets") or [])
|
||||
houses = list(payload.get("houses") or [])
|
||||
|
||||
chart_core: dict[str, Any] = {}
|
||||
house_core: dict[str, Any] = {}
|
||||
section_statuses: dict[str, str] = {}
|
||||
|
||||
for planet in planets:
|
||||
report = call_method(
|
||||
"AllPlanetData",
|
||||
{
|
||||
"args": [
|
||||
{"__vedastro_enum__": "PlanetName", "value": str(planet)},
|
||||
birth_time,
|
||||
]
|
||||
},
|
||||
)
|
||||
chart_core[str(planet)] = {
|
||||
"Status": "Pass" if report.get("status") == "ok" else "Fail",
|
||||
"Payload": {"AllPlanetData": report.get("result")} if report.get("status") == "ok" else report,
|
||||
}
|
||||
if chart_core:
|
||||
section_statuses["chart_core"] = "ok"
|
||||
|
||||
for house in houses:
|
||||
report = call_method(
|
||||
"AllHouseData",
|
||||
{
|
||||
"args": [
|
||||
{"__vedastro_enum__": "HouseName", "value": str(house)},
|
||||
birth_time,
|
||||
]
|
||||
},
|
||||
)
|
||||
house_core[str(house)] = {
|
||||
"Status": "Pass" if report.get("status") == "ok" else "Fail",
|
||||
"Payload": {"AllHouseData": report.get("result")} if report.get("status") == "ok" else report,
|
||||
}
|
||||
if house_core:
|
||||
section_statuses["house_core"] = "ok"
|
||||
|
||||
dasha_report = call_method(
|
||||
"DasaAtRange",
|
||||
{"args": [birth_time, start_time, end_time, levels, precision_hours]},
|
||||
)
|
||||
vimshottari_report = call_method(
|
||||
"DasaAtTime",
|
||||
{"args": [birth_time, check_time, levels]},
|
||||
)
|
||||
chara_report = call_method(
|
||||
"GetCharaDasaAtTime",
|
||||
{"args": [birth_time, check_time]},
|
||||
)
|
||||
strength_report = call_method("AllPlanetStrength", {"args": [birth_time]})
|
||||
ashtakavarga_report = call_method("AshtakvargaLifeMap", {"args": [birth_time]})
|
||||
|
||||
snapshot_sections = {
|
||||
"chart_core": chart_core,
|
||||
"house_core": house_core,
|
||||
"dasha_all": {
|
||||
"Status": "Pass" if dasha_report.get("status") == "ok" else "Fail",
|
||||
"Payload": {"DasaAtRange": dasha_report.get("result")} if dasha_report.get("status") == "ok" else dasha_report,
|
||||
},
|
||||
"vimshottari_now": {
|
||||
"Status": "Pass" if vimshottari_report.get("status") == "ok" else "Fail",
|
||||
"Payload": {"DasaAtTime": vimshottari_report.get("result")} if vimshottari_report.get("status") == "ok" else vimshottari_report,
|
||||
},
|
||||
"chara_dasha_now": {
|
||||
"Status": "Pass" if chara_report.get("status") == "ok" else "Fail",
|
||||
"Payload": {"GetCharaDasaAtTime": chara_report.get("result")} if chara_report.get("status") == "ok" else chara_report,
|
||||
},
|
||||
"shadbala": {
|
||||
"Status": "Pass" if strength_report.get("status") == "ok" else "Fail",
|
||||
"Payload": {"AllPlanetStrength": strength_report.get("result")} if strength_report.get("status") == "ok" else strength_report,
|
||||
},
|
||||
"ashtakavarga": {
|
||||
"Status": "Pass" if ashtakavarga_report.get("status") == "ok" else "Fail",
|
||||
"Payload": {"AshtakvargaLifeMap": ashtakavarga_report.get("result")} if ashtakavarga_report.get("status") == "ok" else ashtakavarga_report,
|
||||
},
|
||||
}
|
||||
|
||||
for section_name in ("dasha_all", "vimshottari_now", "chara_dasha_now", "shadbala", "ashtakavarga"):
|
||||
section_statuses[section_name] = "ok" if snapshot_sections[section_name]["Status"] == "Pass" else "fail"
|
||||
|
||||
filled_sections = [name for name, status in section_statuses.items() if status == "ok"]
|
||||
overall_status = "ok" if filled_sections else "blocked"
|
||||
if filled_sections and len(filled_sections) != len(section_statuses):
|
||||
overall_status = "partial"
|
||||
|
||||
return {
|
||||
"available": bool(filled_sections),
|
||||
"status": overall_status,
|
||||
"method": "official_full_snapshot_bundle",
|
||||
"result": {
|
||||
"snapshot_sections": snapshot_sections,
|
||||
"section_statuses": section_statuses,
|
||||
"coverage": {
|
||||
"source_mode": "official_python_bridge_bundle",
|
||||
"filled_sections": filled_sections,
|
||||
"planet_count": len(chart_core),
|
||||
"house_count": len(house_core),
|
||||
},
|
||||
},
|
||||
"source": "vedastro_python_bridge",
|
||||
}
|
||||
|
||||
return {
|
||||
"available": False,
|
||||
"status": "unsupported_high_value_method",
|
||||
@@ -443,6 +707,7 @@ def main() -> int:
|
||||
parser.add_argument("--method", default="")
|
||||
parser.add_argument("--params-json", default="{}")
|
||||
parser.add_argument("--high-value", default="")
|
||||
parser.add_argument("--list-capabilities", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.print_schema:
|
||||
@@ -464,7 +729,25 @@ def main() -> int:
|
||||
"maps_to": "GetCharaDasaAtTime",
|
||||
"request_contract": ["birth_time", "check_time"],
|
||||
},
|
||||
"official_full_snapshot_bundle": {
|
||||
"maps_to": (
|
||||
"AllPlanetData + AllHouseData + DasaAtRange + DasaAtTime + "
|
||||
"GetCharaDasaAtTime + AllPlanetStrength + AshtakvargaLifeMap"
|
||||
),
|
||||
"request_contract": [
|
||||
"birth_time",
|
||||
"check_time",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"levels?",
|
||||
"precision_hours?",
|
||||
"planets[]",
|
||||
"houses[]",
|
||||
],
|
||||
},
|
||||
}
|
||||
elif args.list_capabilities:
|
||||
result = list_capabilities()
|
||||
elif args.high_value:
|
||||
params = json.loads(args.params_json or "{}")
|
||||
result = call_high_value(args.high_value, params)
|
||||
|
||||
+1000
-22
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user