Files
Jyotisha/scripts/consult_card_domain_facts.py
T
jesse-ux 16f9f86843 fix(consult): date the full-data cache and slim the model card
The cache key now includes the request date, and files older than two days are removed. The model card keeps values only. Parashari aspects are copied from the engine. Saving a chart warms the packet without blocking the response.

Biography severe conflicts rose from 1 to 3, so this is not accepted for merge. Not pushed.
2026-10-09 10:00:45 +08:00

879 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 _house_aspect_rows(view: dict) -> list[dict] | None:
rows = dig(view, "worksheets.graha_drishti.house_aspects")
if not isinstance(rows, list):
return None
return [row for row in rows if isinstance(row, dict)]
def _aspect_targets(row: dict) -> list[int]:
targets = row.get("aspects_to")
if not isinstance(targets, dict):
return []
numbers = []
for key in targets:
try:
numbers.append(int(key))
except (TypeError, ValueError):
continue
return numbers
def _aspected_by(view: dict, house: Any) -> dict[str, Any]:
path = "worksheets.graha_drishti.house_aspects"
rows = _house_aspect_rows(view)
if rows is None or not isinstance(house, int):
return _gap(path)
names = []
for row in rows:
planet = row.get("planet")
if isinstance(planet, str) and house in _aspect_targets(row) and planet not in names:
names.append(planet)
return {"status": "value", "path": path, "value": names, "check": "selection"}
def _aspects_houses(view: dict, name: str) -> dict[str, Any]:
path = "worksheets.graha_drishti.house_aspects"
rows = _house_aspect_rows(view)
if rows is None:
return _gap(path)
for row in rows:
if row.get("planet") == name:
return {"status": "value", "path": path, "value": _aspect_targets(row), "check": "selection"}
return {"status": "value", "path": path, "value": [], "check": "selection"}
def _parashari_aspects(view: dict) -> dict[str, Any]:
citation = "references/signs-and-houses.md §六 行星相位"
leaf = _leaf(view, "worksheets.graha_drishti.house_aspects")
if leaf["status"] != "value":
return {"id": "parashari_aspects", "status": "gap", "citation": citation, "check": "path", "path": leaf.get("path")}
return _sourced("parashari_aspects", citation, leaf)
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": _aspected_by(view, house),
"aspects_houses": _aspects_houses(view, name),
"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},
"aspected_by": _aspected_by(view, number),
}
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", "sourced", "references/signs-and-houses.md §六 行星相位"),
("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")),
_parashari_aspects(view),
_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)