"""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)