feat(consult): copy the full-data packet into a per-domain judgment table
Dialogue cards now take numbers from the same packet as the full-data report. Each of the twelve domains gets one table the card builder assembles. Lookups allow three calls a turn. further_reading is a per-domain technique index. BUG-1293, BUG-1294, BUG-1295.
This commit is contained in:
@@ -0,0 +1,824 @@
|
||||
"""Copy consult-card facts from the full-data packet.
|
||||
|
||||
Values are packet leaves. A missing leaf is a gap. A technique with no
|
||||
``references/`` heading is blocked. D16 and finer, plus Ghati, stay off the
|
||||
card unless the birth minute is confirmed. This module does not assemble the
|
||||
judgment table; the card builder does that from these leaves.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from pl9_full_data_export import read_full_data_domain
|
||||
|
||||
PRECISION_SENTENCE = "出生时间精度不够,不参与"
|
||||
RAHU_NOTE = "罗计不单独定功能吉凶"
|
||||
CONSULTATION_DOMAINS = (
|
||||
"career", "marriage", "wealth", "health", "education", "migration",
|
||||
"family", "parents", "children", "annual", "timing", "general",
|
||||
)
|
||||
PACKET_DOMAIN = {
|
||||
"career": "career",
|
||||
"marriage": "relationship",
|
||||
"wealth": "wealth",
|
||||
"health": "health",
|
||||
"education": "education",
|
||||
"migration": "relocation",
|
||||
"family": "family",
|
||||
"parents": "parents",
|
||||
"children": "children",
|
||||
"annual": "annual",
|
||||
"timing": "timing",
|
||||
"general": "general",
|
||||
}
|
||||
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
|
||||
VARGA_KEYS = {
|
||||
"D2": ("D2_Hora",),
|
||||
"D3": ("D3_Drekkana",),
|
||||
"D4": ("D4_Turyamsa", "D4_Chaturthamsa"),
|
||||
"D5": ("D5_Panchamsa",),
|
||||
"D6": ("D6_Shashthamsa",),
|
||||
"D7": ("D7_Saptamsa",),
|
||||
"D8": ("D8_Ashtamsa",),
|
||||
"D9": ("D9_Navamsa",),
|
||||
"D10": ("D10_Dasamsa",),
|
||||
"D11": ("D11_Rudramsa",),
|
||||
"D12": ("D12_Dwadashamsa",),
|
||||
"D16": ("D16_Shodasamsa",),
|
||||
"D20": ("D20_Vimsamsa",),
|
||||
"D24": ("D24_Siddhamsa", "D24_Chaturvimsamsa"),
|
||||
"D27": ("D27_Bhamsa",),
|
||||
"D30": ("D30_Trimsamsa",),
|
||||
"D40": ("D40_Khavedamsa",),
|
||||
"D45": ("D45_Akshavedamsa",),
|
||||
"D60": ("D60_Shashtyamsa",),
|
||||
}
|
||||
EXTENDED_VARGA = {"D5": "Panchamsa", "D6": "Shashthamsa", "D8": "Ashtamsa", "D11": "Rudramsa"}
|
||||
PRECISION_VARGA = {"D16", "D20", "D24", "D27", "D30", "D40", "D45", "D60"}
|
||||
ROUTER = "references/strict-workflow-router.md"
|
||||
MAPPING = "references/house-domain-planet-mapping.md"
|
||||
CAREER = "references/event_judgment_career.md"
|
||||
MARRIAGE = "references/event_judgment_marriage.md"
|
||||
WEALTH = "references/event_judgment_wealth.md"
|
||||
JAIMINI = "references/jaimini-complete-system.md"
|
||||
DEEP = "references/deep-analysis-complete-workflow.md"
|
||||
VARSHAPHALA = "references/varshaphala-annual-chart-guide.md"
|
||||
PDF = "references/pdf-chart-reading-guide.md"
|
||||
RECTIFY = "references/birth-time-rectification-decision-tree.md"
|
||||
SHODASA = "references/shodasavarga-complete-guide.md"
|
||||
BADHAKA = "references/badhaka-obstacle-planet-guide.md"
|
||||
NARAYANA = "references/bphs-ch48-narayana-dasha.md"
|
||||
VIM = "references/vimshottari_dasha_guide.md"
|
||||
|
||||
|
||||
def dig(node: Any, path: str) -> Any:
|
||||
current = node
|
||||
for part in path.split("."):
|
||||
if isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
elif isinstance(current, list) and part.isdigit():
|
||||
index = int(part)
|
||||
if index < 0 or index >= len(current):
|
||||
return None
|
||||
current = current[index]
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
|
||||
|
||||
def _gap(path: str | None = None) -> dict[str, Any]:
|
||||
item: dict[str, Any] = {"status": "gap", "check": "path"}
|
||||
if path:
|
||||
item["path"] = path
|
||||
return item
|
||||
|
||||
|
||||
def _leaf(view: dict, path: str) -> dict[str, Any]:
|
||||
value = dig(view, path)
|
||||
if value is None or value == "" or value == {} or value == []:
|
||||
return _gap(path)
|
||||
if isinstance(value, (str, int, float, bool)):
|
||||
return {"status": "value", "path": path, "value": value, "check": "path"}
|
||||
encoded = json.dumps(value, ensure_ascii=False, default=str)
|
||||
if "tajika_named_yogas" in encoded or len(encoded) > 800:
|
||||
return {"status": "value", "path": path, "check": "nonempty"}
|
||||
return {"status": "value", "path": path, "value": value, "check": "path"}
|
||||
|
||||
|
||||
def _first(view: dict, paths: list[str]) -> dict[str, Any]:
|
||||
seen = paths[0] if paths else None
|
||||
for path in paths:
|
||||
item = _leaf(view, path)
|
||||
if item["status"] == "value":
|
||||
return item
|
||||
seen = path
|
||||
return _gap(seen)
|
||||
|
||||
|
||||
def minute_confirmed_from_body(body: dict | None) -> bool:
|
||||
"""Same gate as persistedConsultationMode's verified_chart, on the raw body.
|
||||
|
||||
``birth_time_accuracy`` counts only when the request set it. The API's
|
||||
internal default of confirmed is not a confirmation.
|
||||
"""
|
||||
payload = body or {}
|
||||
if payload.get("hour") is None or payload.get("minute") is None:
|
||||
return False
|
||||
status = payload.get("birth_time_status")
|
||||
if status in ("accepted", "confirmed"):
|
||||
return True
|
||||
if "birth_time_accuracy" in payload and payload.get("birth_time_accuracy") == "confirmed":
|
||||
return True
|
||||
# `rectified` is the profile map for an accepted or confirmed minute.
|
||||
# `minute` also covers a reported clock that is not accepted, and that
|
||||
# clock still withholds D16 and Ghati.
|
||||
if payload.get("declared_accuracy") == "rectified":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _planet_base(view: dict, name: str) -> str | None:
|
||||
for path in (
|
||||
f"core_chart.planets.{name}",
|
||||
f"worksheets.d1_rasi_bhava.planets.{name}",
|
||||
):
|
||||
node = dig(view, path)
|
||||
if isinstance(node, dict) and node.get("sign"):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def _functional_role(view: dict, name: str) -> dict[str, Any]:
|
||||
base = "worksheets.strengths_and_scores.functional_benefic_malefic"
|
||||
if name in ("Rahu", "Ketu"):
|
||||
return {"status": "value", "path": base, "value": None, "check": "node_role", "note": RAHU_NOTE}
|
||||
table = dig(view, base)
|
||||
if not isinstance(table, dict):
|
||||
return _gap(base)
|
||||
for key in ("functional_benefics", "functional_malefics", "functional_neutrals", "yogakarakas"):
|
||||
names = table.get(key)
|
||||
if isinstance(names, list) and name in names:
|
||||
return {"status": "value", "path": f"{base}.{key}", "value": key, "check": "membership", "member": name}
|
||||
return {"status": "value", "path": base, "value": None, "check": "node_role", "note": RAHU_NOTE}
|
||||
|
||||
|
||||
def _bav(view: dict, name: str, sign_idx: Any) -> dict[str, Any]:
|
||||
path = f"worksheets.strengths_and_scores.ashtakavarga.bav.{name}.bindus"
|
||||
bindus = dig(view, path)
|
||||
if not isinstance(bindus, list) or not isinstance(sign_idx, int):
|
||||
return _gap(path)
|
||||
if sign_idx < 0 or sign_idx >= len(bindus):
|
||||
return _gap(path)
|
||||
return {"status": "value", "path": path, "index": sign_idx, "value": bindus[sign_idx], "check": "index"}
|
||||
|
||||
|
||||
def _varga_chart(view: dict, code: str) -> tuple[Any, str | None]:
|
||||
for key in VARGA_KEYS.get(code, ()):
|
||||
path = f"worksheets.divisional_and_special_charts.varga_full.{key}"
|
||||
node = dig(view, path)
|
||||
if isinstance(node, dict) and node:
|
||||
return node, path
|
||||
extended = EXTENDED_VARGA.get(code)
|
||||
if extended:
|
||||
path = f"worksheets.divisional_and_special_charts.varga_extended.{extended}"
|
||||
node = dig(view, path)
|
||||
if isinstance(node, dict) and node:
|
||||
return node, path
|
||||
return None, None
|
||||
|
||||
|
||||
def _varga_planet_path(chart_path: str, chart: dict, name: str) -> str | None:
|
||||
direct = chart.get(name)
|
||||
if isinstance(direct, dict):
|
||||
return f"{chart_path}.{name}"
|
||||
planets = chart.get("planets")
|
||||
if isinstance(planets, dict) and isinstance(planets.get(name), dict):
|
||||
return f"{chart_path}.planets.{name}"
|
||||
return None
|
||||
|
||||
|
||||
def _domain_varga_house(view: dict, name: str, code: str, minute_ok: bool) -> dict[str, Any]:
|
||||
if code in PRECISION_VARGA and not minute_ok:
|
||||
return {"status": "withheld", "value": PRECISION_SENTENCE, "check": "withheld"}
|
||||
chart, chart_path = _varga_chart(view, code)
|
||||
if chart_path:
|
||||
planet_path = _varga_planet_path(chart_path, chart, name)
|
||||
if planet_path:
|
||||
house = _leaf(view, f"{planet_path}.house")
|
||||
if house["status"] == "value":
|
||||
return house
|
||||
linked = _leaf(view, f"worksheets.advanced_systems.inter_chart_linkage.{name}.{code}.house")
|
||||
if linked["status"] == "value":
|
||||
return linked
|
||||
return _gap(f"varga.{code}.{name}.house")
|
||||
|
||||
|
||||
def _d9(view: dict, name: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
chart, chart_path = _varga_chart(view, "D9")
|
||||
if not chart_path:
|
||||
return _gap("varga.D9"), _gap("varga.D9.dignity")
|
||||
planet_path = _varga_planet_path(chart_path, chart, name)
|
||||
sign = _leaf(view, f"{planet_path}.sign") if planet_path else _gap(f"{chart_path}.{name}.sign")
|
||||
dignity = _leaf(view, f"{chart_path}._dignity.{name}")
|
||||
if dignity["status"] != "value" and planet_path:
|
||||
dignity = _leaf(view, f"{planet_path}.dignity")
|
||||
return sign, dignity
|
||||
|
||||
|
||||
def _co_tenants(view: dict, name: str, house: Any) -> dict[str, Any]:
|
||||
if not isinstance(house, int):
|
||||
return {"status": "gap", "check": "selection"}
|
||||
names = []
|
||||
for other in PLANETS:
|
||||
if other == name:
|
||||
continue
|
||||
base = _planet_base(view, other)
|
||||
if base and dig(view, f"{base}.house") == house:
|
||||
names.append(other)
|
||||
return {"status": "value", "check": "selection", "value": names}
|
||||
|
||||
|
||||
def _planet_record(view: dict, name: str, varga: str | None, minute_ok: bool) -> dict[str, Any]:
|
||||
base = _planet_base(view, name)
|
||||
if not base:
|
||||
return {"present": False}
|
||||
sign_idx = dig(view, f"{base}.sign_idx")
|
||||
house = dig(view, f"{base}.house")
|
||||
d9_sign, d9_dignity = _d9(view, name)
|
||||
record = {
|
||||
"present": True,
|
||||
"d1_sign": _leaf(view, f"{base}.sign"),
|
||||
"d1_house": _leaf(view, f"{base}.house"),
|
||||
"d1_dignity": _leaf(view, f"{base}.status"),
|
||||
"rasi_lord": _leaf(view, f"{base}.rasi_lord"),
|
||||
"combust": _leaf(view, f"{base}.combust"),
|
||||
"aspected_by": _leaf(view, f"{base}.aspected_by"),
|
||||
"functional_role": _functional_role(view, name),
|
||||
"shadbala_rupas": _leaf(view, f"worksheets.strengths_and_scores.shadbala.planets.{name}.total_rupas"),
|
||||
"shadbala_min": _leaf(view, f"worksheets.strengths_and_scores.shadbala.planets.{name}.min_required"),
|
||||
"bav": _bav(view, name, sign_idx),
|
||||
"d9_sign": d9_sign,
|
||||
"d9_dignity": d9_dignity,
|
||||
"co_tenants": _co_tenants(view, name, house),
|
||||
}
|
||||
if varga:
|
||||
record["domain_varga_house"] = _domain_varga_house(view, name, varga, minute_ok)
|
||||
return record
|
||||
|
||||
|
||||
def _house_record(view: dict, number: int) -> dict[str, Any]:
|
||||
base = f"core_chart.houses.house_{number}"
|
||||
if not isinstance(dig(view, base), dict):
|
||||
base = f"worksheets.d1_rasi_bhava.houses.house_{number}"
|
||||
occupants = []
|
||||
for name in PLANETS:
|
||||
planet_base = _planet_base(view, name)
|
||||
if planet_base and dig(view, f"{planet_base}.house") == number:
|
||||
occupants.append(name)
|
||||
return {
|
||||
"sign": _leaf(view, f"{base}.cusp_sign"),
|
||||
"lord": _leaf(view, f"{base}.lord"),
|
||||
"sav_score": _leaf(view, f"worksheets.strengths_and_scores.ashtakavarga.house_scores.house_{number}.sav_score"),
|
||||
"sav_sodhita": _leaf(view, f"worksheets.strengths_and_scores.ashtakavarga.house_scores.house_{number}.sav_sodhita"),
|
||||
"bhava_bala": _first(view, [
|
||||
f"worksheets.strengths_and_scores.bhava_bala.{number}",
|
||||
f"worksheets.strengths_and_scores.shadbala.bhava_bala.{number}",
|
||||
]),
|
||||
"occupants": {"status": "value", "check": "selection", "value": occupants},
|
||||
}
|
||||
|
||||
|
||||
def _current_period_lord(view: dict, direct: str, timeline: str) -> dict[str, Any]:
|
||||
found = _leaf(view, direct)
|
||||
if found["status"] == "value":
|
||||
return found
|
||||
rows = dig(view, timeline)
|
||||
if isinstance(rows, list):
|
||||
for index, row in enumerate(rows):
|
||||
if isinstance(row, dict) and row.get("is_current") is True and isinstance(row.get("lord"), str):
|
||||
return {"status": "value", "path": f"{timeline}.{index}.lord", "value": row["lord"], "check": "path"}
|
||||
return _gap(direct)
|
||||
|
||||
|
||||
def _timing(view: dict) -> dict[str, Any]:
|
||||
dasha = "worksheets.timing_and_predictive_systems.dasha.current_dasha"
|
||||
ad = _current_period_lord(view, f"{dasha}.antardasha.lord", f"{dasha}.antardasha_timeline")
|
||||
pd = _gap(f"{dasha}.pratyantar.lord")
|
||||
rows = dig(view, f"{dasha}.antardasha_timeline")
|
||||
if isinstance(rows, list):
|
||||
for index, row in enumerate(rows):
|
||||
if not (isinstance(row, dict) and row.get("is_current") is True):
|
||||
continue
|
||||
nested = row.get("pratyantar_dasha_timeline")
|
||||
if isinstance(nested, list):
|
||||
for sub, item in enumerate(nested):
|
||||
if isinstance(item, dict) and item.get("is_current") is True and isinstance(item.get("lord"), str):
|
||||
pd = {
|
||||
"status": "value",
|
||||
"path": f"{dasha}.antardasha_timeline.{index}.pratyantar_dasha_timeline.{sub}.lord",
|
||||
"value": item["lord"],
|
||||
"check": "path",
|
||||
}
|
||||
break
|
||||
narayana = "worksheets.timing_and_predictive_systems.narayana_dasha.current_dasha"
|
||||
return {
|
||||
"md": _leaf(view, f"{dasha}.lord"),
|
||||
"ad": ad,
|
||||
"pd": pd,
|
||||
"narayana_sign": _leaf(view, f"{narayana}.md.sign"),
|
||||
"narayana_lord": _leaf(view, f"{narayana}.md.lord"),
|
||||
"narayana_ad_sign": _leaf(view, f"{narayana}.ad.sign"),
|
||||
"narayana_ad_lord": _leaf(view, f"{narayana}.ad.lord"),
|
||||
}
|
||||
|
||||
|
||||
def _karaka(view: dict, table: str, role: str) -> dict[str, Any]:
|
||||
key = "karaka_table" if table == "7" else "karaka_table_8"
|
||||
folder = "chara_karaka_7" if table == "7" else "chara_karaka_8"
|
||||
return _leaf(view, f"worksheets.advanced_systems.jaimini.{folder}.{key}.{role}.planet")
|
||||
|
||||
|
||||
def _point(view: dict, paths: list[str], *, precision: bool, minute_ok: bool) -> dict[str, Any]:
|
||||
if precision and not minute_ok:
|
||||
return {"status": "withheld", "value": PRECISION_SENTENCE, "check": "withheld", "path": paths[0] if paths else None}
|
||||
return _first(view, paths)
|
||||
|
||||
|
||||
def _blocked(item_id: str, citation: str) -> dict[str, Any]:
|
||||
return {"id": item_id, "status": "blocked", "citation": citation, "check": "blocked"}
|
||||
|
||||
|
||||
def _sourced(item_id: str, citation: str, leaf: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"id": item_id, "citation": citation, **leaf}
|
||||
|
||||
|
||||
# Static §3A catalog. blocked items have no packet path. sourced items name a file.
|
||||
def domain_source_catalog() -> dict[str, list[dict[str, str]]]:
|
||||
shared = [
|
||||
("sudarshana", "sourced", f"{ROUTER} Sudarshana Chakra"),
|
||||
("d9", "sourced", f"{ROUTER} §2 D9 Navamsa"),
|
||||
("bhava_chalit", "sourced", f"{ROUTER} Full Bhava Chalit"),
|
||||
("shadbala", "sourced", f"{ROUTER} §2 Shadbala"),
|
||||
("bhava_bala", "sourced", f"{ROUTER} §2 Shadbala"),
|
||||
("avastha", "sourced", "references/deep-varga-avastha-execution-guide.md"),
|
||||
("vimsopaka", "sourced", f"{DEEP} Vimsopaka"),
|
||||
("ashtakavarga", "sourced", f"{ROUTER} §2 Ashtakavarga"),
|
||||
("functional", "sourced", f"{ROUTER} §2 Functional benefic/malefic"),
|
||||
("parashari_aspects", "blocked", f"{ROUTER} 未找到把 Parashari 星照单独列为参与层的段落"),
|
||||
("jaimini_aspects", "sourced", f"{JAIMINI} Jaimini Drishti"),
|
||||
("argala", "sourced", "references/argala-complete-guide.md"),
|
||||
("dispositor_chain", "sourced", f"{ROUTER} §2 dispositor chain"),
|
||||
("vimshottari", "sourced", VIM),
|
||||
("narayana", "sourced", NARAYANA),
|
||||
("yogini", "sourced", "references/alternative-dasha-systems.md"),
|
||||
("transits", "sourced", f"{ROUTER} §2 Transit"),
|
||||
("double_transit", "sourced", f"{CAREER} §2 Transit / Double Transit"),
|
||||
("yogas_hits", "sourced", "references/event_judgment_skeleton.md"),
|
||||
("kp", "sourced", "决策 8:KP 不参与"),
|
||||
]
|
||||
extra = {
|
||||
"career": [
|
||||
("D10", "sourced", f"{ROUTER} §2 D10 for career"),
|
||||
("A10", "sourced", f"{CAREER} §2 A10 / Karma Pada"),
|
||||
("AL", "sourced", f"{PDF} Arudha Lagna"),
|
||||
("AmK", "sourced", f"{JAIMINI} Amatyakaraka"),
|
||||
("Karakamsha", "sourced", f"{CAREER} §2 Karakamsha"),
|
||||
("Ghati", "sourced", f"{DEEP} Ghati Lagna 权力指标"),
|
||||
("Vighati", "blocked", "references/ 未找到 Vighati 条目"),
|
||||
("natural:Sun", "sourced", f"{CAREER} §2 Sun"),
|
||||
("natural:Saturn", "sourced", f"{CAREER} §2 Saturn"),
|
||||
("natural:Mercury", "sourced", f"{CAREER} §2 Mercury"),
|
||||
("natural:Jupiter", "sourced", f"{CAREER} §2 Jupiter"),
|
||||
("house:10", "sourced", f"{CAREER} §2 10H"),
|
||||
("D1_D9_D10", "sourced", f"{ROUTER} §2 D10 for career"),
|
||||
],
|
||||
"wealth": [
|
||||
("D2", "sourced", f"{WEALTH} §3 D2"),
|
||||
("D11", "sourced", f"{WEALTH} §3 D11"),
|
||||
("Hora", "sourced", f"{DEEP} Hora Lagna 财富指标"),
|
||||
("Indu", "blocked", "references/ 未找到 Indu Lagna 财富条目"),
|
||||
("Sree", "sourced", "references/promise-assessment-templates.md Hora Lagna + Sree Lagna"),
|
||||
("AL2", "blocked", f"{WEALTH} §3 未列出从 AL 起第 2 宫"),
|
||||
("AL11", "blocked", f"{WEALTH} §3 未列出从 AL 起第 11 宫"),
|
||||
("natural:Jupiter", "sourced", f"{WEALTH} §3 Jupiter"),
|
||||
("natural:Venus", "sourced", f"{WEALTH} §3 Venus"),
|
||||
("natural:Mercury", "sourced", f"{WEALTH} §3 Mercury"),
|
||||
("house:2", "sourced", f"{WEALTH} §3 2H"),
|
||||
("house:11", "sourced", f"{WEALTH} §3 11H"),
|
||||
("house:5", "sourced", f"{WEALTH} §3 5H"),
|
||||
("house:9", "sourced", f"{WEALTH} §3 9H"),
|
||||
("house:10", "sourced", f"{WEALTH} §3 10H"),
|
||||
],
|
||||
"marriage": [
|
||||
("D9_7", "sourced", f"{MARRIAGE} §2 D9 7H / 7L"),
|
||||
("UL", "sourced", f"{MARRIAGE} §2 UL"),
|
||||
("UL2", "blocked", f"{MARRIAGE} §2 未列出 UL 起第 2 宫"),
|
||||
("A7", "blocked", f"{MARRIAGE} §2 未列出 A7"),
|
||||
("DK", "sourced", f"{JAIMINI} Darakaraka"),
|
||||
("natural:Venus", "sourced", f"{MARRIAGE} §2 Venus"),
|
||||
("natural:Jupiter", "sourced", f"{MARRIAGE} §2 Jupiter"),
|
||||
("Vivah", "sourced", f"{MARRIAGE} §2 Vivah Saham"),
|
||||
("Mars", "sourced", f"{MARRIAGE} §2 Mars"),
|
||||
("house:7", "sourced", f"{MARRIAGE} §2 7H"),
|
||||
],
|
||||
"health": [
|
||||
("D6", "sourced", f"{ROUTER} §7 D6"),
|
||||
("D8", "sourced", f"{ROUTER} §7 D8"),
|
||||
("D30", "sourced", f"{ROUTER} §7 D30"),
|
||||
("drekkana22", "sourced", f"{SHODASA} 第22Drekkana与寿限"),
|
||||
("navamsa64", "blocked", "references/ 未找到第 64 个 Navamsa 条目"),
|
||||
("Badhaka", "sourced", BADHAKA),
|
||||
("Gulika", "blocked", "references/prashna-complete-guide.md 只在问事章写 Gulika,未列入本命健康参与层"),
|
||||
("Mandi", "blocked", "references/prashna-complete-guide.md 只在问事章写 Mandi,未列入本命健康参与层"),
|
||||
("Pranapada", "blocked", "references/prashna-complete-guide.md 的 Pranapada 是问事替代计算,未列入本命健康参与层"),
|
||||
("natural:Sun", "sourced", f"{ROUTER} §7 Sun"),
|
||||
("natural:Moon", "sourced", f"{ROUTER} §7 Moon"),
|
||||
("natural:Saturn", "sourced", f"{ROUTER} §7 Saturn"),
|
||||
("natural:Mars", "sourced", f"{ROUTER} §7 Mars"),
|
||||
("house:1", "sourced", f"{ROUTER} §7 Lagna"),
|
||||
("house:6", "sourced", f"{ROUTER} §7 6H"),
|
||||
("house:8", "sourced", f"{ROUTER} §7 8H"),
|
||||
("house:12", "sourced", f"{ROUTER} §7 12H"),
|
||||
],
|
||||
"children": [
|
||||
("D7", "sourced", f"{ROUTER} §2 D7 for children"),
|
||||
("PK", "sourced", f"{JAIMINI} Putrakaraka"),
|
||||
("natural:Jupiter", "sourced", f"{MAPPING} 子女 木星"),
|
||||
("house:5", "sourced", f"{MAPPING} 子女 5宫"),
|
||||
],
|
||||
"parents": [
|
||||
("D12", "sourced", f"{ROUTER} §2 D12 for family/ancestral themes"),
|
||||
("MK", "sourced", f"{JAIMINI} Matrikaraka"),
|
||||
("PiK", "sourced", f"{JAIMINI} 可选PiK"),
|
||||
("natural:Sun", "sourced", f"{MAPPING} 父亲 太阳"),
|
||||
("natural:Moon", "sourced", f"{MAPPING} 母亲 月亮"),
|
||||
("house:4", "sourced", f"{MAPPING} 母亲 4宫"),
|
||||
("house:9", "sourced", f"{MAPPING} 父亲 9宫"),
|
||||
],
|
||||
"education": [
|
||||
("D24", "sourced", f"{ROUTER} §2 D24 for education"),
|
||||
("D5", "blocked", "references/ 未找到把 D5 标成学业分盘的段落"),
|
||||
("natural:Mercury", "sourced", f"{MAPPING} 教育 水星"),
|
||||
("natural:Jupiter", "sourced", f"{MAPPING} 教育 木星"),
|
||||
("house:4", "sourced", f"{MAPPING} 教育(基础教育)4宫"),
|
||||
("house:5", "sourced", f"{MAPPING} 智力 5宫"),
|
||||
("house:9", "sourced", f"{MAPPING} 高等教育 9宫"),
|
||||
],
|
||||
"migration": [
|
||||
("D4", "sourced", f"{RECTIFY} 房产/搬迁 D4"),
|
||||
("D12", "blocked", f"{ROUTER} §2 把 D12 标给家族,未标给迁居"),
|
||||
("natural:Rahu", "sourced", f"{MAPPING} 罗睺 外国 / 海外发展"),
|
||||
("house:4", "sourced", f"{RECTIFY} 房产/搬迁"),
|
||||
("house:9", "sourced", f"{MAPPING} 长途旅行 9宫"),
|
||||
("house:12", "sourced", f"{MAPPING} 外国事务 12宫"),
|
||||
],
|
||||
"family": [
|
||||
("D4", "blocked", f"{RECTIFY} 把 D4 标给房产/搬迁,未标给家庭领域"),
|
||||
("D7", "blocked", f"{ROUTER} §2 把 D7 标给子女,未标给家庭领域"),
|
||||
("D12", "sourced", f"{ROUTER} §2 D12 for family/ancestral themes"),
|
||||
("natural", "blocked", f"{MAPPING} 未给出家庭领域的一组自然代表星"),
|
||||
],
|
||||
"annual": [
|
||||
("tajika", "sourced", VARSHAPHALA),
|
||||
("Muntha", "sourced", f"{VARSHAPHALA} Muntha"),
|
||||
("year_lord", "sourced", f"{VARSHAPHALA} 年主星"),
|
||||
("Mudda", "sourced", f"{VARSHAPHALA} Mudda Dasha"),
|
||||
("Sahams", "sourced", VARSHAPHALA),
|
||||
("natural", "blocked", "references/ 未给出流年单独的一组自然代表星"),
|
||||
("house", "blocked", "references/ 未把流年钉在单一本命宫"),
|
||||
],
|
||||
"timing": [
|
||||
("narayana_sub", "sourced", NARAYANA),
|
||||
("year_chart", "sourced", VARSHAPHALA),
|
||||
("natural", "blocked", "references/ 未给出时间领域单独的一组自然代表星"),
|
||||
("house", "blocked", "references/ 未把时间领域钉在单一本命宫"),
|
||||
],
|
||||
"general": [
|
||||
("D1", "sourced", f"{ROUTER} §2 D1 Rashi"),
|
||||
("D9", "sourced", f"{ROUTER} §2 D9 Navamsa"),
|
||||
("D10", "blocked", f"{ROUTER} §2 把 D10 标给事业,未标给综合领域"),
|
||||
("D2", "blocked", f"{ROUTER} §2 把 D2 标给财富,未标给综合领域"),
|
||||
("AL", "sourced", f"{PDF} Arudha Lagna"),
|
||||
("Karakamsha", "sourced", f"{JAIMINI} Karakamsha"),
|
||||
("natural", "blocked", "references/ 未给出综合领域的一组自然代表星"),
|
||||
("house:1", "sourced", f"{MAPPING} 第1宫"),
|
||||
],
|
||||
}
|
||||
catalog = {}
|
||||
for domain, rows in extra.items():
|
||||
catalog[domain] = [
|
||||
{"id": item_id, "status": status, "citation": citation}
|
||||
for item_id, status, citation in ([*shared, *rows])
|
||||
]
|
||||
return catalog
|
||||
|
||||
|
||||
def _shared_participation(view: dict, minute_ok: bool) -> list[dict[str, Any]]:
|
||||
items = [
|
||||
_sourced("sudarshana", f"{ROUTER} Sudarshana Chakra", _leaf(view, "worksheets.divisional_and_special_charts.sudarshana")),
|
||||
_sourced("d9", f"{ROUTER} §2 D9 Navamsa", _leaf(view, "worksheets.divisional_and_special_charts.varga_full.D9_Navamsa")),
|
||||
_sourced("bhava_chalit", f"{ROUTER} Full Bhava Chalit", _first(view, [
|
||||
"worksheets.d1_rasi_bhava.bhava_chalit",
|
||||
"worksheets.divisional_and_special_charts.bhava_chalit",
|
||||
])),
|
||||
_sourced("shadbala", f"{ROUTER} §2 Shadbala", _leaf(view, "worksheets.strengths_and_scores.shadbala")),
|
||||
_sourced("bhava_bala", f"{ROUTER} §2 Shadbala", _leaf(view, "worksheets.strengths_and_scores.bhava_bala")),
|
||||
_sourced("avastha", "references/deep-varga-avastha-execution-guide.md", _leaf(view, "worksheets.advanced_systems.avasthas")),
|
||||
_sourced("vimsopaka", f"{DEEP} Vimsopaka", _leaf(view, "worksheets.strengths_and_scores.vimsopaka")),
|
||||
_sourced("ashtakavarga", f"{ROUTER} §2 Ashtakavarga", _leaf(view, "worksheets.strengths_and_scores.ashtakavarga")),
|
||||
_sourced("functional", f"{ROUTER} §2 Functional benefic/malefic", _leaf(view, "worksheets.strengths_and_scores.functional_benefic_malefic")),
|
||||
_blocked("parashari_aspects", f"{ROUTER} 未找到把 Parashari 星照单独列为参与层的段落"),
|
||||
_sourced("jaimini_aspects", f"{JAIMINI} Jaimini Drishti", _first(view, [
|
||||
"worksheets.advanced_systems.jaimini.rashi_aspects",
|
||||
"worksheets.advanced_systems.jaimini.drishti",
|
||||
])),
|
||||
_sourced("argala", "references/argala-complete-guide.md", _leaf(view, "worksheets.advanced_systems.argala")),
|
||||
_sourced("dispositor_chain", f"{ROUTER} §2 dispositor chain", _leaf(view, "worksheets.advanced_systems.dispositor_chains")),
|
||||
_sourced("vimshottari", VIM, _leaf(view, "worksheets.timing_and_predictive_systems.dasha.current_dasha.lord")),
|
||||
_sourced("narayana", NARAYANA, _leaf(view, "worksheets.timing_and_predictive_systems.narayana_dasha.current_dasha.md.sign")),
|
||||
_sourced("yogini", "references/alternative-dasha-systems.md", _leaf(view, "worksheets.timing_and_predictive_systems.yogini_dasha.current.planet")),
|
||||
_sourced("transits", f"{ROUTER} §2 Transit", _leaf(view, "worksheets.timing_and_predictive_systems.transit_multi_reference")),
|
||||
_sourced("double_transit", f"{CAREER} §2 Transit / Double Transit", _leaf(view, "worksheets.timing_and_predictive_systems.double_transit_pac")),
|
||||
_sourced("yogas_hits", "references/event_judgment_skeleton.md", _first(view, [
|
||||
"worksheets.advanced_systems.yoga.hits",
|
||||
"worksheets.advanced_systems.yogas.hits",
|
||||
])),
|
||||
{"id": "kp", "status": "value", "value": "不参与", "citation": "决策 8:KP 不参与", "check": "label"},
|
||||
]
|
||||
del minute_ok
|
||||
return items
|
||||
|
||||
|
||||
def _varga_item(view: dict, item_id: str, citation: str, code: str, minute_ok: bool) -> dict[str, Any]:
|
||||
if code in PRECISION_VARGA and not minute_ok:
|
||||
return {"id": item_id, "status": "withheld", "value": PRECISION_SENTENCE, "citation": citation, "check": "withheld"}
|
||||
_chart, path = _varga_chart(view, code)
|
||||
if not path:
|
||||
return {"id": item_id, "status": "gap", "path": f"varga.{code}", "citation": citation, "check": "path"}
|
||||
return _sourced(item_id, citation, _leaf(view, path))
|
||||
|
||||
|
||||
def _domain_participation(view: dict, domain: str, minute_ok: bool) -> list[dict[str, Any]]:
|
||||
lagna = "worksheets.divisional_and_special_charts.special_lagnas"
|
||||
annual = "worksheets.timing_and_predictive_systems"
|
||||
if domain == "career":
|
||||
return [
|
||||
_varga_item(view, "D10", f"{ROUTER} §2 D10 for career", "D10", minute_ok),
|
||||
_sourced("A10", f"{CAREER} §2 A10 / Karma Pada", _leaf(view, f"{lagna}.A10_Karma_Pada.sign")),
|
||||
_sourced("AL", f"{PDF} Arudha Lagna", _leaf(view, f"{lagna}.Arudha_Lagna.sign")),
|
||||
_sourced("AmK", f"{JAIMINI} Amatyakaraka", _karaka(view, "7", "Amatyakaraka")),
|
||||
_sourced("Karakamsha", f"{CAREER} §2 Karakamsha", _leaf(view, "worksheets.advanced_systems.jaimini.karakamsha")),
|
||||
_sourced("Ghati", f"{DEEP} Ghati Lagna 权力指标", _point(view, [f"{lagna}.Ghati_Lagna.sign"], precision=True, minute_ok=minute_ok)),
|
||||
_blocked("Vighati", "references/ 未找到 Vighati 条目"),
|
||||
_sourced("D1_D9_D10", f"{ROUTER} §2 D10 for career", _leaf(view, "worksheets.advanced_systems.inter_chart_linkage")),
|
||||
]
|
||||
if domain == "wealth":
|
||||
return [
|
||||
_varga_item(view, "D2", f"{WEALTH} §3 D2", "D2", minute_ok),
|
||||
_varga_item(view, "D11", f"{WEALTH} §3 D11", "D11", minute_ok),
|
||||
_sourced("Hora", f"{DEEP} Hora Lagna 财富指标", _leaf(view, f"{lagna}.Hora_Lagna.sign")),
|
||||
_blocked("Indu", "references/ 未找到 Indu Lagna 财富条目"),
|
||||
_sourced("Sree", "references/promise-assessment-templates.md Hora Lagna + Sree Lagna", _first(view, [
|
||||
f"{lagna}.Sree_Lagna.sign",
|
||||
f"{lagna}.Sri_Lagna.sign",
|
||||
])),
|
||||
_blocked("AL2", f"{WEALTH} §3 未列出从 AL 起第 2 宫"),
|
||||
_blocked("AL11", f"{WEALTH} §3 未列出从 AL 起第 11 宫"),
|
||||
]
|
||||
if domain == "marriage":
|
||||
return [
|
||||
_varga_item(view, "D9_7", f"{MARRIAGE} §2 D9 7H / 7L", "D9", minute_ok),
|
||||
_sourced("UL", f"{MARRIAGE} §2 UL", _leaf(view, f"{lagna}.Upapada_Lagna.sign")),
|
||||
_blocked("UL2", f"{MARRIAGE} §2 未列出 UL 起第 2 宫"),
|
||||
_blocked("A7", f"{MARRIAGE} §2 未列出 A7"),
|
||||
_sourced("DK", f"{JAIMINI} Darakaraka", _karaka(view, "7", "Darakaraka")),
|
||||
_sourced("Vivah", f"{MARRIAGE} §2 Vivah Saham", _first(view, [
|
||||
f"{annual}.annual_tajika_pack.sahams.data.vivah_saham",
|
||||
"worksheets.advanced_systems.sahams.vivah_saham",
|
||||
])),
|
||||
_sourced("Mars", f"{MARRIAGE} §2 Mars", _leaf(view, "core_chart.planets.Mars.sign")),
|
||||
]
|
||||
if domain == "health":
|
||||
return [
|
||||
_varga_item(view, "D6", f"{ROUTER} §7 D6", "D6", minute_ok),
|
||||
_varga_item(view, "D8", f"{ROUTER} §7 D8", "D8", minute_ok),
|
||||
_varga_item(view, "D30", f"{ROUTER} §7 D30", "D30", minute_ok),
|
||||
_sourced("drekkana22", f"{SHODASA} 第22Drekkana与寿限", _first(view, [
|
||||
"worksheets.divisional_and_special_charts.sensitive_points.drekkana_22",
|
||||
"worksheets.divisional_and_special_charts.varga_full.D3_Drekkana",
|
||||
])),
|
||||
_blocked("navamsa64", "references/ 未找到第 64 个 Navamsa 条目"),
|
||||
_sourced("Badhaka", BADHAKA, _first(view, [
|
||||
"worksheets.advanced_systems.badhaka",
|
||||
"worksheets.d1_rasi_bhava.badhaka",
|
||||
])),
|
||||
_blocked("Gulika", "references/prashna-complete-guide.md 只在问事章写 Gulika,未列入本命健康参与层"),
|
||||
_blocked("Mandi", "references/prashna-complete-guide.md 只在问事章写 Mandi,未列入本命健康参与层"),
|
||||
_blocked("Pranapada", "references/prashna-complete-guide.md 的 Pranapada 是问事替代计算,未列入本命健康参与层"),
|
||||
]
|
||||
if domain == "children":
|
||||
return [
|
||||
_varga_item(view, "D7", f"{ROUTER} §2 D7 for children", "D7", minute_ok),
|
||||
_sourced("PK", f"{JAIMINI} Putrakaraka", _karaka(view, "7", "Putrakaraka")),
|
||||
]
|
||||
if domain == "parents":
|
||||
return [
|
||||
_varga_item(view, "D12", f"{ROUTER} §2 D12 for family/ancestral themes", "D12", minute_ok),
|
||||
_sourced("MK", f"{JAIMINI} Matrikaraka", _karaka(view, "7", "Matrikaraka")),
|
||||
_sourced("PiK", f"{JAIMINI} 可选PiK", _karaka(view, "8", "Pitrukaraka")),
|
||||
]
|
||||
if domain == "education":
|
||||
return [
|
||||
_varga_item(view, "D24", f"{ROUTER} §2 D24 for education", "D24", minute_ok),
|
||||
_blocked("D5", "references/ 未找到把 D5 标成学业分盘的段落"),
|
||||
]
|
||||
if domain == "migration":
|
||||
return [
|
||||
_varga_item(view, "D4", f"{RECTIFY} 房产/搬迁 D4", "D4", minute_ok),
|
||||
_blocked("D12", f"{ROUTER} §2 把 D12 标给家族,未标给迁居"),
|
||||
]
|
||||
if domain == "family":
|
||||
return [
|
||||
_blocked("D4", f"{RECTIFY} 把 D4 标给房产/搬迁,未标给家庭领域"),
|
||||
_blocked("D7", f"{ROUTER} §2 把 D7 标给子女,未标给家庭领域"),
|
||||
_varga_item(view, "D12", f"{ROUTER} §2 D12 for family/ancestral themes", "D12", minute_ok),
|
||||
]
|
||||
if domain == "annual":
|
||||
return [
|
||||
_sourced("tajika", VARSHAPHALA, _leaf(view, f"{annual}.tajika_yogas")),
|
||||
_sourced("Muntha", f"{VARSHAPHALA} Muntha", _first(view, [
|
||||
f"{annual}.annual_tajika_pack.muntha",
|
||||
f"{annual}.tajika.muntha",
|
||||
])),
|
||||
_sourced("year_lord", f"{VARSHAPHALA} 年主星", _first(view, [
|
||||
f"{annual}.annual_tajika_pack.year_lord",
|
||||
f"{annual}.tajika.year_lord",
|
||||
])),
|
||||
_sourced("Mudda", f"{VARSHAPHALA} Mudda Dasha", _first(view, [
|
||||
f"{annual}.annual_tajika_pack.mudda_dasha",
|
||||
f"{annual}.tajika.mudda_dasha",
|
||||
])),
|
||||
_sourced("Sahams", VARSHAPHALA, _first(view, [
|
||||
f"{annual}.annual_tajika_pack.sahams",
|
||||
"worksheets.advanced_systems.sahams",
|
||||
])),
|
||||
]
|
||||
if domain == "timing":
|
||||
return [
|
||||
_sourced("narayana_sub", NARAYANA, _leaf(view, f"{annual}.narayana_dasha.current_dasha.ad.sign")),
|
||||
_sourced("year_chart", VARSHAPHALA, _first(view, [
|
||||
f"{annual}.annual_tajika_pack",
|
||||
f"{annual}.tajika",
|
||||
])),
|
||||
]
|
||||
if domain == "general":
|
||||
return [
|
||||
_varga_item(view, "D1", f"{ROUTER} §2 D1 Rashi", "D1", minute_ok),
|
||||
_varga_item(view, "D9", f"{ROUTER} §2 D9 Navamsa", "D9", minute_ok),
|
||||
_blocked("D10", f"{ROUTER} §2 把 D10 标给事业,未标给综合领域"),
|
||||
_blocked("D2", f"{ROUTER} §2 把 D2 标给财富,未标给综合领域"),
|
||||
_sourced("AL", f"{PDF} Arudha Lagna", _leaf(view, f"{lagna}.Arudha_Lagna.sign")),
|
||||
_sourced("Karakamsha", f"{JAIMINI} Karakamsha", _leaf(view, "worksheets.advanced_systems.jaimini.karakamsha")),
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def _factors(domain: str, timing: dict[str, Any], karakas: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
catalog = domain_source_catalog()[domain]
|
||||
factors: list[dict[str, Any]] = []
|
||||
for row in catalog:
|
||||
item_id = row["id"]
|
||||
if item_id.startswith("house:"):
|
||||
factors.append({"id": item_id, "kind": "house", "house": int(item_id.split(":")[1]), "citation": row["citation"]})
|
||||
elif item_id.startswith("natural:"):
|
||||
factors.append({"id": item_id, "kind": "planet", "planet": item_id.split(":")[1], "citation": row["citation"], "role": "natural"})
|
||||
elif item_id == "natural":
|
||||
factors.append({"id": item_id, "kind": "blocked", "citation": row["citation"]})
|
||||
elif item_id == "house":
|
||||
factors.append({"id": item_id, "kind": "blocked", "citation": row["citation"]})
|
||||
for item_id, role in (("AmK", "Amatyakaraka"), ("DK", "Darakaraka"), ("PK", "Putrakaraka"), ("MK", "Matrikaraka")):
|
||||
leaf = karakas.get(role)
|
||||
if leaf and leaf.get("status") == "value" and isinstance(leaf.get("value"), str):
|
||||
if any(row["id"] == item_id for row in catalog):
|
||||
factors.append({"id": f"jaimini:{item_id}", "kind": "planet", "planet": leaf["value"], "citation": f"{JAIMINI} {role}", "role": item_id})
|
||||
elif any(row["id"] == item_id for row in catalog):
|
||||
factors.append({"id": f"jaimini:{item_id}", "kind": "gap", "citation": f"{JAIMINI} {role}", "path": leaf.get("path") if leaf else None})
|
||||
pik = karakas.get("Pitrukaraka")
|
||||
if any(row["id"] == "PiK" for row in catalog):
|
||||
if pik and pik.get("status") == "value" and isinstance(pik.get("value"), str):
|
||||
factors.append({"id": "jaimini:PiK", "kind": "planet", "planet": pik["value"], "citation": f"{JAIMINI} 可选PiK", "role": "PiK"})
|
||||
else:
|
||||
factors.append({"id": "jaimini:PiK", "kind": "gap", "citation": f"{JAIMINI} 可选PiK", "path": (pik or {}).get("path")})
|
||||
if not any(row["id"].startswith("jaimini") or row["id"] in {"AmK", "DK", "PK", "MK", "PiK"} for row in catalog):
|
||||
factors.append({"id": "jaimini", "kind": "blocked", "citation": f"{JAIMINI} 未给出该领域的 Jaimini 代表星"})
|
||||
for key, label in (("md", "Vimshottari MD"), ("ad", "Vimshottari AD"), ("pd", "Vimshottari PD")):
|
||||
leaf = timing.get(key) or {}
|
||||
if leaf.get("status") == "value" and isinstance(leaf.get("value"), str):
|
||||
factors.append({"id": f"vimshottari:{key}", "kind": "planet", "planet": leaf["value"], "citation": VIM, "role": label})
|
||||
else:
|
||||
factors.append({"id": f"vimshottari:{key}", "kind": "gap", "citation": VIM, "path": leaf.get("path")})
|
||||
sign = timing.get("narayana_sign") or {}
|
||||
lord = timing.get("narayana_lord") or {}
|
||||
factors.append({"id": "narayana:md", "kind": "sign", "citation": NARAYANA, "sign": sign, "lord": lord})
|
||||
if lord.get("status") == "value" and isinstance(lord.get("value"), str):
|
||||
factors.append({"id": "narayana:lord", "kind": "planet", "planet": lord["value"], "citation": NARAYANA, "role": "Narayana MD lord"})
|
||||
else:
|
||||
factors.append({"id": "narayana:lord", "kind": "gap", "citation": NARAYANA, "path": lord.get("path")})
|
||||
return factors
|
||||
|
||||
|
||||
def build_domain_facts(packet: dict, domain: str, *, minute_confirmed: bool) -> dict[str, Any]:
|
||||
if domain not in PACKET_DOMAIN:
|
||||
raise KeyError(domain)
|
||||
view = read_full_data_domain(packet, PACKET_DOMAIN[domain])
|
||||
varga = {
|
||||
"career": "D10", "wealth": "D2", "marriage": "D9", "health": "D6",
|
||||
"children": "D7", "parents": "D12", "education": "D24", "migration": "D4",
|
||||
"family": "D4", "annual": "D1", "timing": "D9", "general": "D10",
|
||||
}[domain]
|
||||
planets = {name: _planet_record(view, name, varga, minute_confirmed) for name in PLANETS}
|
||||
houses = {}
|
||||
for row in domain_source_catalog()[domain]:
|
||||
if row["id"].startswith("house:"):
|
||||
number = int(row["id"].split(":")[1])
|
||||
houses[str(number)] = _house_record(view, number)
|
||||
timing = _timing(view)
|
||||
karakas = {
|
||||
"Amatyakaraka": _karaka(view, "7", "Amatyakaraka"),
|
||||
"Darakaraka": _karaka(view, "7", "Darakaraka"),
|
||||
"Putrakaraka": _karaka(view, "7", "Putrakaraka"),
|
||||
"Matrikaraka": _karaka(view, "7", "Matrikaraka"),
|
||||
"Pitrukaraka": _karaka(view, "8", "Pitrukaraka"),
|
||||
}
|
||||
participation = _shared_participation(view, minute_confirmed) + _domain_participation(view, domain, minute_confirmed)
|
||||
facts = {
|
||||
"schema": "consult-card-facts-v1",
|
||||
"domain": domain,
|
||||
"packet_domain": PACKET_DOMAIN[domain],
|
||||
"header_note": RAHU_NOTE,
|
||||
"precision_sentence": PRECISION_SENTENCE,
|
||||
"minute_confirmed": minute_confirmed,
|
||||
"domain_varga": varga,
|
||||
"planets": planets,
|
||||
"houses": houses,
|
||||
"timing": timing,
|
||||
"factors": _factors(domain, timing, karakas),
|
||||
"participation": participation,
|
||||
}
|
||||
encoded = json.dumps(facts, ensure_ascii=False, default=str)
|
||||
if "tajika_named_yogas" in encoded:
|
||||
raise RuntimeError("consult card facts must not read tajika_named_yogas")
|
||||
return facts
|
||||
|
||||
|
||||
def build_consult_card_facts(
|
||||
packet: dict,
|
||||
domains: list[str],
|
||||
*,
|
||||
minute_confirmed: bool | None,
|
||||
body: dict | None = None,
|
||||
) -> dict[str, Any]:
|
||||
confirmed = minute_confirmed_from_body(body) if minute_confirmed is None else minute_confirmed
|
||||
rows = []
|
||||
for domain in domains:
|
||||
if domain not in PACKET_DOMAIN:
|
||||
continue
|
||||
rows.append(build_domain_facts(packet, domain, minute_confirmed=confirmed))
|
||||
return {
|
||||
"schema": "consult-card-facts-v1",
|
||||
"minute_confirmed": confirmed,
|
||||
"domains": rows,
|
||||
"gaps": [] if rows else ["consult_domain_missing"],
|
||||
}
|
||||
|
||||
|
||||
def assert_fact_leaves(packet: dict, facts: dict[str, Any]) -> None:
|
||||
"""Every copied leaf equals the packet. Derived selections are not leaves."""
|
||||
view = read_full_data_domain(packet, facts["packet_domain"])
|
||||
|
||||
def check(item: Any) -> None:
|
||||
if isinstance(item, list):
|
||||
for entry in item:
|
||||
check(entry)
|
||||
return
|
||||
if not isinstance(item, dict):
|
||||
return
|
||||
kind = item.get("check")
|
||||
if kind == "path" and item.get("status") == "value":
|
||||
assert dig(view, item["path"]) == item["value"], item["path"]
|
||||
elif kind == "index" and item.get("status") == "value":
|
||||
assert dig(view, item["path"])[item["index"]] == item["value"], item["path"]
|
||||
elif kind == "nonempty" and item.get("status") == "value":
|
||||
assert dig(view, item["path"]) not in (None, {}, [], ""), item["path"]
|
||||
elif kind == "membership" and item.get("status") == "value":
|
||||
assert item["member"] in dig(view, item["path"]), item["path"]
|
||||
elif kind == "withheld":
|
||||
assert item.get("value") == PRECISION_SENTENCE
|
||||
for key, value in item.items():
|
||||
if key in ("value", "path", "citation"):
|
||||
continue
|
||||
check(value)
|
||||
|
||||
check(facts)
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Cache the full-data report packet for the consult card.
|
||||
|
||||
The key is chart identity + ayanamsa + node mode + engine version.
|
||||
The file name is a hash. Birth data stays inside the scratch file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Callable
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CACHE_SCHEMA = "consult-full-data-cache-v1"
|
||||
SLOW_MISS_SECONDS = 20.0
|
||||
|
||||
PacketBuilder = Callable[[dict[str, Any]], dict[str, Any]]
|
||||
|
||||
|
||||
def cache_dir() -> Path:
|
||||
path = ROOT / "scratch" / "local" / "consult_full_data_cache"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def engine_version() -> str:
|
||||
try:
|
||||
from jyotish_vedic import __version__ as version
|
||||
except Exception:
|
||||
return "unknown"
|
||||
return str(version)
|
||||
|
||||
|
||||
def _number(value: Any) -> int | float:
|
||||
number = float(value)
|
||||
if number == int(number):
|
||||
return int(number)
|
||||
return number
|
||||
|
||||
|
||||
def identity_from_body(body: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Birth fields are required. Missing keys raise KeyError so the caller can gap."""
|
||||
node = body.get("node_mode", body.get("nodeMode", "mean"))
|
||||
ayanamsa = body.get("ayanamsa", "lahiri")
|
||||
second = body.get("second", 0)
|
||||
return {
|
||||
"schema": CACHE_SCHEMA,
|
||||
"year": int(body["year"]),
|
||||
"month": int(body["month"]),
|
||||
"day": int(body["day"]),
|
||||
"hour": _number(body["hour"]),
|
||||
"minute": _number(body["minute"]),
|
||||
"second": _number(second if second is not None else 0),
|
||||
"lat": _number(body["lat"]),
|
||||
"lon": _number(body["lon"]),
|
||||
"tz": _number(body["tz"]),
|
||||
"ayanamsa": str(ayanamsa),
|
||||
"node_mode": str(node or "mean"),
|
||||
"engine_version": engine_version(),
|
||||
}
|
||||
|
||||
|
||||
def cache_key(identity: dict[str, Any]) -> str:
|
||||
canonical = json.dumps(identity, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _cache_path(identity: dict[str, Any]) -> Path:
|
||||
return cache_dir() / f"{cache_key(identity)}.json"
|
||||
|
||||
|
||||
def get_or_build(
|
||||
identity: dict[str, Any],
|
||||
builder: PacketBuilder | None = None,
|
||||
*,
|
||||
clock: Callable[[], float] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return the packet. A miss calls builder once and stores the packet."""
|
||||
now = clock or time.perf_counter
|
||||
started = now()
|
||||
path = _cache_path(identity)
|
||||
if path.is_file():
|
||||
packet = json.loads(path.read_text(encoding="utf-8"))
|
||||
elapsed = now() - started
|
||||
return {"packet": packet, "hit": True, "elapsed_s": elapsed, "slow": False}
|
||||
if builder is None:
|
||||
builder = build_full_data_packet
|
||||
packet = builder(identity)
|
||||
temporary = path.with_suffix(".json.tmp")
|
||||
temporary.write_text(json.dumps(packet, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
|
||||
temporary.replace(path)
|
||||
elapsed = now() - started
|
||||
return {
|
||||
"packet": packet,
|
||||
"hit": False,
|
||||
"elapsed_s": elapsed,
|
||||
"slow": elapsed > SLOW_MISS_SECONDS,
|
||||
}
|
||||
|
||||
|
||||
def build_full_data_packet(identity: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Packet-only build. No markdown render and no sanitized rewrite."""
|
||||
from datetime import date
|
||||
|
||||
from calculation_profile_contract import attach_calculation_profile
|
||||
from jyotish_engine import build_pl9_style_export_packet, cmd_full_reading
|
||||
|
||||
today = date.today()
|
||||
args = SimpleNamespace(
|
||||
year=int(identity["year"]),
|
||||
month=int(identity["month"]),
|
||||
day=int(identity["day"]),
|
||||
hour=float(identity["hour"]),
|
||||
minute=float(identity["minute"]),
|
||||
second=float(identity["second"]),
|
||||
lat=float(identity["lat"]),
|
||||
lon=float(identity["lon"]),
|
||||
tz=float(identity["tz"]),
|
||||
ayanamsa=identity["ayanamsa"],
|
||||
node_mode=identity["node_mode"],
|
||||
today=today.isoformat(),
|
||||
target_year=today.year,
|
||||
age=today.year - int(identity["year"]),
|
||||
transit_date=today.isoformat(),
|
||||
birth_time_accuracy="confirmed",
|
||||
)
|
||||
reading = cmd_full_reading(args)
|
||||
if not isinstance(reading, dict) or reading.get("error"):
|
||||
raise RuntimeError("full_reading_unavailable")
|
||||
packet = build_pl9_style_export_packet(reading)
|
||||
return attach_calculation_profile(packet, args)
|
||||
|
||||
|
||||
def _requested_domains(body: dict[str, Any]) -> list[str]:
|
||||
# Card domains (parents, children) are not the engine theme (family).
|
||||
raw = body.get("consult_card_domains")
|
||||
if raw is None:
|
||||
raw = body.get("themes", body.get("theme"))
|
||||
if raw is None:
|
||||
return []
|
||||
if isinstance(raw, str):
|
||||
return [raw]
|
||||
if isinstance(raw, list):
|
||||
return [str(item) for item in raw if isinstance(item, str)]
|
||||
return []
|
||||
|
||||
|
||||
def attach_consult_full_data(result: dict[str, Any], body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""Thin hook for the consultation response. Failures do not break the chat."""
|
||||
payload = dict(body or {})
|
||||
if payload.get("include_consult_card_facts") is not True:
|
||||
return result
|
||||
try:
|
||||
from consult_card_domain_facts import build_consult_card_facts
|
||||
except ImportError:
|
||||
from scripts.consult_card_domain_facts import build_consult_card_facts
|
||||
try:
|
||||
identity = identity_from_body(payload)
|
||||
cached = get_or_build(identity)
|
||||
facts = build_consult_card_facts(
|
||||
cached["packet"],
|
||||
_requested_domains(payload),
|
||||
minute_confirmed=None,
|
||||
body=payload,
|
||||
)
|
||||
facts["timing"] = {
|
||||
"hit": bool(cached["hit"]),
|
||||
"elapsed_s": cached["elapsed_s"],
|
||||
"slow": bool(cached["slow"]),
|
||||
}
|
||||
except Exception as exc:
|
||||
facts = {
|
||||
"schema": "consult-card-facts-v1",
|
||||
"gaps": ["consult_full_data_unavailable"],
|
||||
"error_type": type(exc).__name__,
|
||||
}
|
||||
attached = dict(result)
|
||||
attached["consult_card_facts"] = facts
|
||||
return attached
|
||||
@@ -4331,11 +4331,16 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
|
||||
})
|
||||
|
||||
def _compute_consultation_workflow(self, body):
|
||||
return execute_consultation_workflow(
|
||||
result = execute_consultation_workflow(
|
||||
self,
|
||||
body=dict(body or {}),
|
||||
surface=body.get('surface', 'api_web'),
|
||||
)
|
||||
try:
|
||||
from scripts.consult_full_data_cache import attach_consult_full_data
|
||||
except ImportError:
|
||||
from consult_full_data_cache import attach_consult_full_data
|
||||
return attach_consult_full_data(result, body or {})
|
||||
|
||||
def _build_audited_remedies_from_guided_topics(self, guided_topics):
|
||||
if not isinstance(guided_topics, list):
|
||||
|
||||
Reference in New Issue
Block a user