Files
Jyotisha/scripts/annual_tajika_pack.py
T
Jesse_Chen bab0718700
Independent Staging Quality Gate / validate (push) Failing after 9m41s
Independent Staging Quality Gate / publish (push) Has been skipped
feat(report): ship full-mode longform appendix beside the five-chapter report
Web export now calls the same full pack as the long skill report and caches an owner-only Markdown download. Appendix failure stays unavailable and does not change the main report status.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 11:10:44 +08:00

652 lines
27 KiB
Python
Executable File

"""Annual Varshaphala / Tajika report pack contract.
The pack normalizes existing annual producers into a stable, audit-friendly
shape. It does not adjudicate conflicting annual methods yet; that belongs to
the next conflict-gate layer.
"""
from __future__ import annotations
from datetime import datetime, timedelta
from types import SimpleNamespace
from typing import Any
try: # pragma: no cover - import path differs under CLI vs pytest
from calculation_profile_contract import build_calculation_profile
except ImportError: # pragma: no cover
from scripts.calculation_profile_contract import build_calculation_profile
try: # pragma: no cover - import path differs under CLI vs pytest
from tajika_named_yoga_authority import build_tajika_named_yoga_authority
except ImportError: # pragma: no cover
from scripts.tajika_named_yoga_authority import build_tajika_named_yoga_authority
SCHEMA = "jyotish.annual_tajika_pack.v1"
def build_annual_tajika_pack(
payload: dict[str, Any],
*,
solar_return_report: dict[str, Any] | None = None,
tajika_report: dict[str, Any] | None = None,
pyjhora_replay: dict[str, Any] | None = None,
vedastro_reference: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build a JSON-safe annual pack for arbitrary birth payloads."""
profile = build_calculation_profile(payload)
args = _args_from_payload(payload)
solar_report = solar_return_report if solar_return_report is not None else _safe_solar_return(args)
tajika_report = tajika_report if tajika_report is not None else _safe_tajika(args)
solar_return = _field_from_report(solar_report, "solar_return", "Solar Return")
annual_chart = _annual_chart_from_solar_report(solar_report)
muntha = _field_with_conflict_gate("muntha", solar_report, tajika_report)
year_lord = _field_with_conflict_gate("year_lord", solar_report, tajika_report)
tajika_yogas = _tajika_yogas_field(solar_report)
sahams = _field_from_report(solar_report, "sahams", "Sahams")
mudda_dasha = _dasha_field("Mudda Dasha", solar_report, tajika_report, "mudda_dasha")
patyayini_dasha = _dasha_field("Patyayini Dasha", solar_report, tajika_report, "patyayini_dasha")
pack = {
"schema": SCHEMA,
"profile": {
**profile,
"target_year": int(payload["target_year"]),
},
"solar_return": solar_return,
"annual_chart": annual_chart,
"muntha": muntha,
"year_lord": year_lord,
"tajika_yogas": tajika_yogas,
"sahams": sahams,
"mudda_dasha": mudda_dasha,
"patyayini_dasha": patyayini_dasha,
"monthly_windows": _monthly_windows(mudda_dasha),
"external_engine_comparison": _external_engine_comparison(pyjhora_replay, vedastro_reference),
"report_sections": _report_sections(
profile,
muntha=muntha,
year_lord=year_lord,
tajika_yogas=tajika_yogas,
sahams=sahams,
),
"exports": {},
"audit": _audit(profile, solar_report, tajika_report, pyjhora_replay, vedastro_reference),
}
pack["exports"] = _exports(pack)
return pack
def _args_from_payload(payload: dict[str, Any]) -> SimpleNamespace:
birth = dict(payload.get("birth") or {})
settings = dict(payload.get("settings") or {})
year, month, day = [int(part) for part in str(birth["date"]).split("-")]
time_parts = [int(part) for part in str(birth.get("time") or "00:00:00").split(":")]
while len(time_parts) < 3:
time_parts.append(0)
age = payload.get("age")
if age is None and payload.get("target_year") is not None:
age = int(payload["target_year"]) - year
return SimpleNamespace(
year=year,
month=month,
day=day,
hour=time_parts[0],
minute=time_parts[1],
second=time_parts[2],
lat=float(birth["latitude"]),
lon=float(birth["longitude"]),
tz=_offset_to_hours(birth.get("utc_offset")),
target_year=int(payload["target_year"]),
age=age,
mode="all",
ayanamsa=settings.get("ayanamsa", "lahiri"),
node_mode=settings.get("node_mode", "mean"),
house_system=settings.get("house_system", "whole_sign"),
position_mode=settings.get("position_mode", "legacy"),
dasha_year_days=settings.get("dasha_year_days", 365.25),
solar_return_location_mode=settings.get("solar_return_location_mode", "birth_place"),
annual_year_policy=settings.get("annual_year_policy", "solar_return_exact"),
)
def _offset_to_hours(offset: Any) -> float:
if offset is None:
return 0.0
if isinstance(offset, (int, float)):
return float(offset)
text = str(offset).strip()
sign = -1 if text.startswith("-") else 1
text = text.lstrip("+-")
hours, minutes = [int(part) for part in text.split(":")[:2]]
return sign * (hours + minutes / 60)
def _safe_solar_return(args: SimpleNamespace) -> dict[str, Any]:
try:
from scripts.cmd_solar_return import cmd_solar_return
except ImportError: # pragma: no cover
from cmd_solar_return import cmd_solar_return
try:
result = cmd_solar_return(args)
return result if isinstance(result, dict) else {"error": "solar_return_non_dict"}
except Exception as exc: # pragma: no cover - defensive boundary
return {"error": str(exc)}
def _safe_tajika(args: SimpleNamespace) -> dict[str, Any]:
try:
from scripts.jyotish_engine import cmd_tajika
except ImportError: # pragma: no cover
from jyotish_engine import cmd_tajika
try:
result = cmd_tajika(args)
return result if isinstance(result, dict) else {"error": "tajika_non_dict"}
except Exception as exc: # pragma: no cover - defensive boundary
return {"error": str(exc)}
def _field_from_report(report: dict[str, Any], key: str, label: str) -> dict[str, Any]:
if not isinstance(report, dict) or report.get("error"):
return {"status": "blocked", "producer": label, "reason": report.get("error", "producer_failed")}
value = report.get(key)
if value is None:
return {"status": "blocked", "producer": label, "reason": f"{key}_missing"}
if isinstance(value, dict) and value.get("status") in {"blocked", "conflict", "not_applicable", "parameter_sensitive", "partial_verified", "verified"}:
normalized = {
"status": value.get("status"),
"producer": label,
"data": value,
}
if value.get("reason") is not None:
normalized["reason"] = value.get("reason")
return normalized
return {"status": "partial_verified", "producer": label, "data": value}
def _annual_chart_from_solar_report(report: dict[str, Any]) -> dict[str, Any]:
field = _field_from_report(report, "sr_chart_info", "Solar Return Annual Chart")
if field["status"] == "partial_verified":
field["data_contract"] = "normalized_from_sr_chart_info"
return field
def _tajika_yogas_field(solar_report: dict[str, Any]) -> dict[str, Any]:
if not isinstance(solar_report, dict) or solar_report.get("error"):
return _field_from_report(solar_report, "tajika_yogas", "Tajika Yogas")
authority = _build_governed_tajika_named_yoga_authority(solar_report)
if authority is None:
return _field_from_report(solar_report, "tajika_yogas", "Tajika Yogas")
return {
"status": "partial_verified" if authority.get("status") == "authority_ready" else authority.get("status", "blocked"),
"producer": "Tajika Named Yoga Authority",
"data": authority,
"data_contract": "governed_named_yoga_authority_surface",
}
def _build_governed_tajika_named_yoga_authority(solar_report: dict[str, Any]) -> dict[str, Any] | None:
planets = _extract_tajika_motion_planets(solar_report)
if not planets:
return None
authority = build_tajika_named_yoga_authority(planets)
if not isinstance(authority, dict):
return None
return authority
def _extract_tajika_motion_planets(solar_report: dict[str, Any]) -> dict[str, dict[str, float]]:
chart = solar_report.get("chart") if isinstance(solar_report.get("chart"), dict) else {}
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
extracted: dict[str, dict[str, float]] = {}
for name in ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"):
payload = planets.get(name)
if not isinstance(payload, dict):
continue
longitude = payload.get("degree_raw", payload.get("degree"))
speed = payload.get("speed")
if longitude is None or speed is None:
continue
extracted[name] = {"longitude": float(longitude), "speed": float(speed)}
return extracted
def _first_available_field(label: str, *reports_and_key: Any) -> dict[str, Any]:
*reports, key = reports_and_key
producers = []
for report in reports:
field = _field_from_report(report, key, label)
producers.append(field)
if field["status"] == "partial_verified":
field["producers_checked"] = producers
return field
return {"status": "blocked", "producer": label, "reason": f"{key}_missing", "producers_checked": producers}
def _field_with_conflict_gate(key: str, solar_report: dict[str, Any], tajika_report: dict[str, Any]) -> dict[str, Any]:
values = []
for producer, report in (("solar_return", solar_report), ("tajika", tajika_report)):
if isinstance(report, dict) and report.get(key) is not None and not report.get("error"):
values.append({"producer": producer, "value": report[key]})
if len(values) >= 2 and values[0]["value"] != values[1]["value"]:
return {
"status": "conflict",
"field": key,
"values": values,
"reason": "same_profile_annual_producers_disagree",
}
if values:
return {
"status": "partial_verified",
"producer": values[0]["producer"],
"data": values[0]["value"],
"producers_checked": values,
}
return {
"status": "blocked",
"field": key,
"reason": f"{key}_missing",
"producers_checked": values,
}
def _dasha_field(label: str, solar_report: dict[str, Any], tajika_report: dict[str, Any], key: str) -> dict[str, Any]:
field = _first_available_field(label, solar_report, tajika_report, key)
data = field.get("data") if isinstance(field.get("data"), dict) else {}
periods = data.get("periods") or data.get("dasha_sequence") or []
field["periods"] = periods if isinstance(periods, list) else []
return field
def _monthly_windows(mudda_dasha: dict[str, Any]) -> list[dict[str, Any]]:
windows = []
for idx, period in enumerate(mudda_dasha.get("periods", []), start=1):
if not isinstance(period, dict):
continue
windows.append(
{
"index": idx,
"source": "mudda_dasha",
"lord": period.get("lord"),
"duration_months": period.get("months"),
"status": "partial_verified",
}
)
return windows
def _report_sections(
profile: dict[str, Any],
*,
muntha: dict[str, Any] | None = None,
year_lord: dict[str, Any] | None = None,
tajika_yogas: dict[str, Any] | None = None,
sahams: dict[str, Any] | None = None,
) -> dict[str, Any]:
blocked_fields = [
name
for name, field in (("muntha", muntha), ("year_lord", year_lord))
if isinstance(field, dict) and field.get("status") == "blocked"
]
conflict_fields = [
name
for name, field in (("muntha", muntha), ("year_lord", year_lord))
if isinstance(field, dict) and field.get("status") == "conflict"
]
all_flagged_fields = [
name
for name in (*blocked_fields, *conflict_fields)
if name
]
muntha_brief = _field_brief("Muntha", muntha)
year_lord_brief = _field_brief("Year Lord", year_lord)
tajika_brief = _field_brief("Tajika Yogas", tajika_yogas)
sahams_brief = _field_brief("Sahams", sahams)
quick_takeaways = [item for item in (muntha_brief, year_lord_brief) if item]
narrative_preview = [
"年度层已经有可读壳层,但仍需要带着冲突标签阅读。",
muntha_brief,
year_lord_brief,
]
if blocked_fields:
summary_status = "blocked"
summary_reason = "interpretive annual narrative waits for blocked field closure"
thematic_status = "blocked"
thematic_reason = "annual technique closure incomplete"
elif conflict_fields:
summary_status = "parameter_sensitive"
summary_reason = "interpretive annual narrative remains conflict-labeled until producer adjudication closes"
thematic_status = "parameter_sensitive"
thematic_reason = "annual technique conflict requires labeled reading"
else:
summary_status = "partial_verified"
summary_reason = "annual shell is readable but still awaits broader external parity closure"
thematic_status = "partial_verified"
thematic_reason = "annual technique shell available with current native evidence"
return {
"executive_summary": {
"status": summary_status,
"reason": summary_reason,
"blocked_fields": all_flagged_fields,
"summary_lines": narrative_preview,
"quick_takeaways": quick_takeaways,
},
"thematic_narrative": {
"status": thematic_status,
"reason": thematic_reason,
"highlights": [
"Solar Return / Tajika annual shell is available.",
"Muntha and Year Lord are visible, but Year Lord remains producer-disagree sensitive.",
"The report should read annual structure first, then the evidence appendix.",
],
},
"evidence_appendix": {
"status": "partial_verified",
"profile_id": profile["profile_id"],
"field_briefs": {
"muntha": muntha_brief,
"year_lord": year_lord_brief,
"tajika_yogas": tajika_brief,
"sahams": sahams_brief,
},
"must_not_claim": ["exact_annual_event_prediction"],
},
}
def _exports(pack: dict[str, Any]) -> dict[str, Any]:
exports = {
"json": {
"schema": pack["schema"],
"profile_id": pack["profile"]["profile_id"],
"field_statuses": {
key: value.get("status")
for key, value in pack.items()
if isinstance(value, dict) and "status" in value
},
},
"markdown": _markdown_summary(pack),
"ai_evidence_bundle": {
"schema": "jyotish.annual_tajika_pack.ai_evidence.v1",
"profile_id": pack["profile"]["profile_id"],
"allowed_claim_status": "blocked_until_conflict_gate",
"raw_field_paths": [
"solar_return",
"annual_chart",
"muntha",
"year_lord",
"tajika_yogas",
"sahams",
"mudda_dasha",
"patyayini_dasha",
],
},
}
try: # pragma: no cover - import path differs under CLI vs pytest
from report_pack_contract import normalize_report_pack_contract
except ImportError: # pragma: no cover
from scripts.report_pack_contract import normalize_report_pack_contract
exports["unified_report_pack_contract"] = normalize_report_pack_contract(
{**pack, "exports": exports},
pack_id="annual_tajika_pack",
)
return exports
def _markdown_summary(pack: dict[str, Any]) -> str:
rows = ["| field | status |", "| --- | --- |"]
for key in (
"solar_return",
"annual_chart",
"muntha",
"year_lord",
"tajika_yogas",
"sahams",
"mudda_dasha",
"patyayini_dasha",
"external_engine_comparison",
):
field = pack[key]
status = field.get('status')
if key == "year_lord":
values = field.get("values") or []
if values:
first = values[0].get("value") if isinstance(values[0], dict) else {}
if isinstance(first, dict):
status = f"{status}: {first.get('year_lord') or first.get('year_lord_sign')}"
rows.append(f"| {key} | {status} |")
summary = [
"",
"### Annual Reading Preview",
"",
f"- blocked fields: {', '.join((pack['report_sections']['executive_summary'].get('blocked_fields') or [])) or 'none'}",
]
return "\n".join(rows + summary)
def _external_engine_comparison(
pyjhora_replay: dict[str, Any] | None,
vedastro_reference: dict[str, Any] | None,
) -> dict[str, Any]:
if not pyjhora_replay and not vedastro_reference:
return {
"status": "blocked",
"reason": "external annual replay not integrated in annual_tajika_pack.v1",
"engines": [],
}
comparison: dict[str, Any] = {"status": "blocked", "engines": []}
statuses = []
if pyjhora_replay:
comparison["engines"].append("PyJHora/JHora")
pyjhora = dict(pyjhora_replay)
patyayini = pyjhora.get("patyayini_dasha")
if isinstance(patyayini, dict):
patyayini = dict(patyayini)
patyayini["normalized_rows"] = _normalize_patyayini_replay_rows(patyayini)
pyjhora["patyayini_dasha"] = patyayini
comparison["pyjhora"] = pyjhora
statuses.append(pyjhora_replay.get("status", "blocked"))
year_lord_replay = pyjhora_replay.get("year_lord_replay")
if isinstance(year_lord_replay, dict):
raw_artifact_path = year_lord_replay.get("raw_artifact_path")
comparison["evidence_scope"] = "pyjhora_behavior_only"
comparison["parity_status"] = "not_multiengine_parity"
comparison["raw_evidence_paths"] = [raw_artifact_path] if raw_artifact_path else []
comparison["raw_evidence_status"] = (
"archived_artifact" if raw_artifact_path else "runtime_observation_not_archived"
)
comparison["year_lord_replay"] = {
key: year_lord_replay[key]
for key in (
"status",
"reason",
"callable",
"pyjhora_version",
"effective_ayanamsa",
"request_hash",
"node_mode",
)
if key in year_lord_replay
}
if vedastro_reference:
comparison["engines"].append("VedAstro official")
comparison["vedastro"] = _sanitize_vedastro_reference(vedastro_reference)
statuses.append(comparison["vedastro"].get("status", "blocked"))
comparison["status"] = "partial_verified" if any(str(s).startswith("partial_verified") for s in statuses) else "blocked"
return comparison
def _normalize_patyayini_replay_rows(field: dict[str, Any]) -> list[dict[str, Any]]:
"""Expose PyJHora tuple shape without inferring a local Patyayini contract."""
raw = field.get("raw") if isinstance(field.get("raw"), dict) else {}
periods = raw.get("periods") if isinstance(raw.get("periods"), list) else []
rows = []
for order, period in enumerate(periods, start=1):
if not isinstance(period, (list, tuple)) or len(period) != 3:
continue
codes, boundary, duration = period
if not isinstance(codes, (list, tuple)) or len(codes) != 2:
continue
if not isinstance(boundary, (list, tuple)) or len(boundary) != 4:
continue
try:
year, month, day = (int(value) for value in boundary[:3])
hour_decimal = float(boundary[3])
boundary_display = (datetime(year, month, day) + timedelta(hours=hour_decimal)).strftime("%Y-%m-%d %H:%M:%S")
except (TypeError, ValueError, OverflowError):
continue
rows.append({
"order": order,
"main_code": codes[0],
"sub_code": codes[1],
"boundary_components": {
"year": year,
"month": month,
"day": day,
"hour_decimal": hour_decimal,
},
"boundary_display": boundary_display,
"duration_raw": duration,
"timezone_semantics": "not_returned_by_pyjhora_tuple",
"boundary_semantics": "unresolved_external_tuple_boundary",
"evidence_status": "pyjhora_behavior_only / not_multiengine_parity",
})
return rows
def _sanitize_vedastro_reference(reference: dict[str, Any]) -> dict[str, Any]:
allowed = {
"engine",
"status",
"chart_core",
"dasha_all",
"varshaphala_status",
"supported_annual_methods",
"secret_redaction",
}
sanitized = {key: value for key, value in reference.items() if key in allowed}
sanitized.setdefault("engine", "VedAstro official")
sanitized.setdefault("status", "blocked")
sanitized.setdefault("varshaphala_status", "blocked")
sanitized.setdefault("supported_annual_methods", [])
sanitized["secret_redaction"] = {"secret_material_included": False}
return sanitized
def _field_brief(label: str, field: dict[str, Any] | None) -> str:
if not isinstance(field, dict):
return f"{label}: unavailable"
status = field.get("status", "blocked")
if status == "conflict":
values = field.get("values") or []
render_values = []
for item in values[:2]:
if not isinstance(item, dict):
continue
value = item.get("value")
if isinstance(value, dict):
render_values.append(
", ".join(
str(part)
for part in (
value.get("year_lord"),
value.get("year_lord_sign"),
value.get("muntha_sign"),
value.get("muntha_lord"),
)
if part
)
)
if render_values:
return f"{label}: conflict between { ' vs '.join(render_values) }"
if status == "partial_verified":
data = field.get("data") if isinstance(field.get("data"), dict) else {}
if label == "Muntha":
return f"{label}: {data.get('muntha_sign') or data.get('muntha_sign_idx')} / {data.get('muntha_lord') or 'unknown'}"
if label == "Year Lord":
return f"{label}: {data.get('year_lord') or 'unknown'}"
if label == "Tajika Yogas":
authority_status = data.get("status")
if authority_status == "authority_ready":
summary = data.get("summary") if isinstance(data.get("summary"), dict) else {}
row_count = summary.get("row_count")
supported = data.get("supported_named_yogas") if isinstance(data.get("supported_named_yogas"), list) else []
supported_text = "/".join(str(item) for item in supported) if supported else "governed named yogas"
return (
f"Tajika Yogas: governed authority surface visible for {supported_text}"
+ (f" ({row_count} rows)" if row_count is not None else "")
)
return "Tajika Yogas: annual candidate structures visible"
if label == "Sahams":
return "Sahams: annual sensitive points visible"
return f"{label}: {status}"
def _audit(
profile: dict[str, Any],
solar_report: dict[str, Any],
tajika_report: dict[str, Any],
pyjhora_replay: dict[str, Any] | None = None,
vedastro_reference: dict[str, Any] | None = None,
) -> dict[str, Any]:
muntha_status = _field_with_conflict_gate("muntha", solar_report, tajika_report).get("status", "blocked")
year_lord_status = _field_with_conflict_gate("year_lord", solar_report, tajika_report).get("status", "blocked")
audit = {
"profile_id": profile["profile_id"],
"technique_audit": [
{"technique": "Calculation Profile", "status": "verified", "profile_id": profile["profile_id"]},
{"technique": "Solar Return", "status": _producer_status(solar_report)},
{"technique": "Annual Chart", "status": "partial_verified" if solar_report.get("sr_chart_info") else "blocked"},
{"technique": "Muntha", "status": muntha_status},
{"technique": "Year Lord", "status": year_lord_status},
{"technique": "Tajika Yogas", "status": _tajika_yogas_field(solar_report).get("status", "blocked")},
{"technique": "Sahams", "status": "partial_verified" if solar_report.get("sahams") else "blocked"},
{"technique": "Mudda Dasha", "status": "partial_verified" if (solar_report.get("mudda_dasha") or tajika_report.get("mudda_dasha")) else "blocked"},
{"technique": "Patyayini Dasha", "status": "blocked", "reason": "native producer not integrated"},
{"technique": "External Engine Comparison", "status": "blocked", "reason": "pending Task 5/6"},
],
}
if pyjhora_replay:
year_lord_replay = pyjhora_replay.get("year_lord_replay")
audit["technique_audit"].append(
{
"technique": "PyJHora Annual Replay",
"status": pyjhora_replay.get("status", "blocked"),
"license_boundary": pyjhora_replay.get("license_boundary"),
**(
{
"evidence_scope": "pyjhora_behavior_only",
"parity_status": "not_multiengine_parity",
"raw_artifact_path": year_lord_replay.get("raw_artifact_path"),
"raw_evidence_status": (
"archived_artifact"
if year_lord_replay.get("raw_artifact_path")
else "runtime_observation_not_archived"
),
"year_lord_replay_status": year_lord_replay.get("status", "blocked"),
"node_mode": year_lord_replay.get("node_mode"),
}
if isinstance(year_lord_replay, dict)
else {}
),
}
)
if vedastro_reference:
vedastro = _sanitize_vedastro_reference(vedastro_reference)
audit["technique_audit"].append(
{
"technique": "VedAstro Annual Boundary",
"status": vedastro.get("status", "blocked"),
"varshaphala_status": vedastro.get("varshaphala_status", "blocked"),
}
)
return audit
def _producer_status(report: dict[str, Any]) -> str:
return "blocked" if report.get("error") else "partial_verified"