- KP monthly support read the birth-balance lord every month (no current_dasha in the issue #137 timeline); now the running lords, matching upstream - ported prediction_verdict_matrix.py and references/rectification_evidence_contract_v1.json (the contract was always blocked as file missing) - English audit statuses run / not_run written as Ran / Not run (run is also a packet key) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
831 lines
49 KiB
Python
831 lines
49 KiB
Python
"""Export chapters the astrologer's pl9-export has and ours lacked.
|
||
|
||
产品 2026-11-07 与占星师上游同步 (items 7 and 8):
|
||
|
||
* KP house view of the significators, Cuspal Promise, the career / finance /
|
||
relationship KP house checks, the Shodashvarga dignity matrix and the
|
||
planet-to-Bhava aspect table;
|
||
* three-year KP monthly support, the ephemeris key-month index and the
|
||
annual extra points index;
|
||
* the rectification contract chapter: birth-data provenance, timing precision,
|
||
event replay, the prediction verdict matrix, the ±1-minute birth-time
|
||
sensitivity matrix with its minute ranking, and the rectification evidence
|
||
domain registry. Without events the replay, the verdict matrix and the minute
|
||
ranking stay blocked, as upstream.
|
||
|
||
Values come from the packet. Two upstream producers are called here when the
|
||
packet lacks their output: ``prediction_verdict_matrix`` (ported unchanged) and
|
||
the ±1-minute recast through ``flexible_birth_time_profile`` — upstream builds
|
||
that matrix for every report, ours only for an unconfirmed time. PyJHora replay
|
||
tables are not ported (AGPL boundary).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime, timedelta
|
||
from typing import Any
|
||
|
||
try:
|
||
from scripts.report_render.appendix_language import planet_name as _planet_name
|
||
from scripts.report_render.appendix_language import sign_name
|
||
from scripts.report_render.audit_chapters import _clean, _status
|
||
from scripts.report_render.audit_chapters import _table as _audit_table
|
||
from scripts.report_render.text import _md_cell
|
||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||
from report_render.appendix_language import planet_name as _planet_name
|
||
from report_render.appendix_language import sign_name
|
||
from report_render.audit_chapters import _clean, _status
|
||
from report_render.audit_chapters import _table as _audit_table
|
||
from report_render.text import _md_cell
|
||
|
||
KP_HEADING = {"zh": "KP 宫位显著星、Cuspal Promise 与分盘尊贵", "en": "KP House Significators, Cuspal Promise and Varga Dignity"}
|
||
MONTHLY_HEADING = {"zh": "三年流月、星历重点月份与年度附加点", "en": "Three-Year Monthly Support, Ephemeris Key Months and Annual Extra Points"}
|
||
RECT_HEADING = {"zh": "校时与断事合同", "en": "Rectification and Prediction Contract"}
|
||
|
||
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
|
||
HOUSE_THEMES = {
|
||
1: ("自我、健康、性格", "self, health, temperament"),
|
||
2: ("财富、家庭、言语", "wealth, family, speech"),
|
||
3: ("沟通、旅行、兄弟", "communication, short travel, siblings"),
|
||
4: ("母亲、房产、情感", "mother, property, feelings"),
|
||
5: ("子女、投资、创意", "children, speculation, creativity"),
|
||
6: ("身体负荷、对手、债务", "health load, rivals, debt"),
|
||
7: ("婚姻、合作、伴侣", "marriage, partnership, spouse"),
|
||
8: ("转型、意外、遗产", "transformation, sudden events, inheritance"),
|
||
9: ("命运、父亲、灵性", "fortune, father, dharma"),
|
||
10: ("事业、声望、成就", "career, reputation, achievement"),
|
||
11: ("收益、社交、愿望", "gains, networks, wishes"),
|
||
12: ("损失、外迁、解脱", "loss, moving abroad, release"),
|
||
}
|
||
KP_DOMAINS = (
|
||
("career", (2, 6, 10, 11), ("事业", "Career"),
|
||
("2宫看资源与收入,6宫看工作任务与压力,10宫看事业角色,11宫看结果兑现与回收。",
|
||
"2nd: resources and income; 6th: daily work and pressure; 10th: career role; 11th: results and returns.")),
|
||
("wealth", (2, 5, 9, 11), ("财务", "Finance"),
|
||
("2宫看资金与资源,5宫看判断与投机,9宫看机会与支持,11宫看回报兑现。",
|
||
"2nd: money and resources; 5th: judgement and speculation; 9th: opportunity and support; 11th: returns.")),
|
||
("relationship", (2, 7, 11), ("关系", "Relationship"),
|
||
("2宫看关系投入,7宫看伴侣/合作轴线,11宫看关系结果是否落地。",
|
||
"2nd: what goes into the relationship; 7th: the partner axis; 11th: whether it lands.")),
|
||
)
|
||
DIGNITY_VARGAS = (
|
||
("D1", None), ("D2", "D2_Hora"), ("D3", "D3_Drekkana"), ("D4", "D4_Turyamsa"), ("D7", "D7_Saptamsa"),
|
||
("D9", "D9_Navamsa"), ("D10", "D10_Dasamsa"), ("D12", "D12_Dwadashamsa"), ("D16", "D16_Shodasamsa"),
|
||
("D20", "D20_Vimsamsa"), ("D24", "D24_Siddhamsa"), ("D27", "D27_Bhamsa"), ("D30", "D30_Trimsamsa"),
|
||
("D40", "D40_Khavedamsa"), ("D45", "D45_Akshavedamsa"), ("D60", "D60_Shashtyamsa"),
|
||
)
|
||
DIGNITY_ZH = {
|
||
"Exalted": "入旺", "Debilitated": "落陷", "Own Sign": "自宫", "Moolatrikona": "三方宫",
|
||
"Neutral": "中性", "Friendly Sign": "入友", "Enemy Sign": "入敌", "Great Friend": "极友", "Great Enemy": "极敌",
|
||
}
|
||
SENSITIVITY_KEYS = (
|
||
"D1.ascendant", "D3.ascendant", "D4.ascendant", "D6.ascendant", "D7.ascendant", "D9.ascendant",
|
||
"D10.ascendant", "D11.ascendant", "D12.ascendant", "D16.ascendant", "D24.ascendant", "D30.ascendant",
|
||
"D40.ascendant", "D45.ascendant", "D60.ascendant", "arudha.UL", "arudha.A7", "arudha.A10", "KP.cusp_observation",
|
||
)
|
||
WORDS = {
|
||
# birth provenance
|
||
"not_recorded": ("未记录", "not recorded"),
|
||
"unverified_user_coordinates": ("用户提供坐标,未核验", "user coordinates, unverified"),
|
||
# timing precision claims
|
||
"exact_day_event": ("确定事件日期", "a certain event date"),
|
||
"exact_clock_event": ("确定事件钟点", "a certain event clock time"),
|
||
"dated_outcome_promise": ("带日期的结果承诺", "a dated outcome promise"),
|
||
"candidate_observation_window": ("候选观察窗口", "candidate observation window"),
|
||
"guaranteed_weekly_outcome": ("必然发生的周级事件", "a certain weekly outcome"),
|
||
"guaranteed_monthly_outcome": ("必然发生的月级事件", "a certain monthly outcome"),
|
||
"annual_solar_return_structure": ("独立太阳返照的年度结构", "the annual solar-return structure"),
|
||
"year_as_verified_event_calendar": ("把年度写成已验证的事件日历", "the year as a verified event calendar"),
|
||
# event replay fields
|
||
"event_id": ("事件编号", "event id"),
|
||
"year_or_date": ("年份或日期", "year or date"),
|
||
"domain": ("领域", "domain"),
|
||
"event_description": ("事件描述", "event description"),
|
||
"outcome_or_observation": ("结果或观察", "outcome or observation"),
|
||
"evidence_strength": ("证据强度", "evidence strength"),
|
||
"user_history_not_supplied": ("未提供过往事件", "no past events supplied"),
|
||
"required_before_execution": ("执行前必须满足", "required before running"),
|
||
"transit_multi_reference": ("四参考点行运", "four-reference transits"),
|
||
"annual_tajika": ("年度 Tajika", "annual Tajika"),
|
||
# verdict matrix
|
||
"career": ("事业", "career"), "relationship": ("关系", "relationship"), "wealth": ("财富", "wealth"),
|
||
"health": ("健康", "health"), "relocation": ("迁移", "relocation"), "education": ("教育", "education"),
|
||
"quarter_window": ("季度窗口", "quarter window"),
|
||
"season_window": ("季节窗口", "season window"),
|
||
"low_confidence_warning_only": ("仅低置信提醒", "low-confidence warning only"),
|
||
"case_replay_bounded_window": ("回放约束窗口", "replay-bounded window"),
|
||
"event_replay_missing": ("缺事件回放", "no event replay"),
|
||
"parameter_sensitivity_open": ("参数敏感未闭合", "parameter sensitivity open"),
|
||
"strength_filter_missing": ("缺力量过滤", "no strength filter"),
|
||
"multi_system_consensus_ready": ("多系统一致可用", "multi-system consensus ready"),
|
||
"stable": ("稳定", "stable"),
|
||
# sensitivity matrix
|
||
"stable_across_window": ("窗口内稳定", "stable across the window"),
|
||
"minute_sensitive": ("随分钟变化", "changes by minute"),
|
||
"review_not_materialized": ("尚无校时复核记录", "no rectification review on record"),
|
||
"record_rectification_review": ("记录校时复核", "record a rectification review"),
|
||
"forbidden": ("禁止", "forbidden"),
|
||
"research_cross_reference_only": ("仅作研究交叉参照", "research cross-reference only"),
|
||
}
|
||
DOMAIN_EN = {
|
||
"family_origin": "Parents and family of origin", "siblings_childhood": "Siblings and childhood",
|
||
"home_property": "Home, moves and property", "education_qualification": "Education and qualifications",
|
||
"career_trajectory": "Career path", "wealth_retention": "Wealth gained and kept",
|
||
"relationship_marriage": "Relationships, marriage and partnership", "children_family": "Children",
|
||
"health_adversity": "Health, accidents and adversity", "migration_foreign": "Migration and abroad",
|
||
"comfort_vehicles": "Comfort, vehicles and living standard", "career": "Career path",
|
||
"wealth": "Wealth gained and kept", "relationship": "Relationships, marriage and partnership",
|
||
"children": "Children", "relocation": "Migration and moving abroad", "education": "Education and qualifications",
|
||
"health": "Health, accidents and adversity", "family": "Parents and family of origin",
|
||
}
|
||
|
||
|
||
NODE_ZH = {"Rahu": "北交点", "Ketu": "南交点"}
|
||
|
||
|
||
def planet_name(value: Any, language: str) -> str:
|
||
"""The body's short node names (北交点 / 南交点); a slash inside a slash-joined cell reads as two planets."""
|
||
if language != "en" and str(value or "") in NODE_ZH:
|
||
return NODE_ZH[str(value)]
|
||
return _planet_name(value, language)
|
||
|
||
|
||
def _table(header: tuple[str, ...], rows: list[tuple[str, ...]]) -> list[str]:
|
||
"""Table cells start with a capital in English: a lone lower-case word reads as a raw packet key."""
|
||
fixed = [tuple(cell[:1].upper() + cell[1:] if cell[:1].isascii() and cell[:1].islower() else cell for cell in row)
|
||
for row in rows]
|
||
return _audit_table(header, fixed)
|
||
|
||
|
||
def _colon(language: str) -> str:
|
||
return ": " if language == "en" else ":"
|
||
|
||
|
||
def _lang(language: str) -> int:
|
||
return 1 if language == "en" else 0
|
||
|
||
|
||
def _dict(value: Any) -> dict:
|
||
return value if isinstance(value, dict) else {}
|
||
|
||
|
||
def _list(value: Any) -> list:
|
||
return value if isinstance(value, list) else []
|
||
|
||
|
||
def _word(value: Any, language: str) -> str:
|
||
key = str(value if value is not None else "").strip()
|
||
if key in WORDS:
|
||
return WORDS[key][_lang(language)]
|
||
if key.lower() in ("true", "false"):
|
||
return {"true": ("是", "yes"), "false": ("否", "no")}[key.lower()][_lang(language)]
|
||
return _clean(key, language) if key else "—"
|
||
|
||
|
||
def _words(values: Any, language: str, sep: str | None = None) -> str:
|
||
sep = sep or ("、" if language != "en" else ", ")
|
||
items = [_word(item, language) for item in _list(values) if item not in (None, "")]
|
||
return sep.join(items) or "—"
|
||
|
||
|
||
def _yes(value: Any, language: str) -> str:
|
||
if isinstance(value, bool):
|
||
return ("是", "yes")[_lang(language)] if value else ("否", "no")[_lang(language)]
|
||
return _word(value, language)
|
||
|
||
|
||
def _sheet(packet: dict, name: str) -> dict:
|
||
return _dict(_dict(packet.get("worksheets")).get(name))
|
||
|
||
|
||
def _significator(value: Any, language: str) -> str:
|
||
"""A/B/C/D cell: planets for the house view, house numbers for the planet view."""
|
||
items = value if isinstance(value, (list, tuple)) else ([value] if value not in (None, "", []) else [])
|
||
out = []
|
||
for item in items:
|
||
if isinstance(item, (int, float)) and not isinstance(item, bool):
|
||
out.append(str(int(item)))
|
||
elif item not in (None, ""):
|
||
out.append(planet_name(item, language))
|
||
return ("、" if language != "en" else ", ").join(out) or "—"
|
||
|
||
|
||
def _kp_houses(packet: dict) -> dict[int, dict]:
|
||
kp = _dict(_sheet(packet, "advanced_systems").get("kp"))
|
||
houses = _dict(kp.get("houses"))
|
||
out = {}
|
||
for house in range(1, 13):
|
||
row = _dict(houses.get(str(house)) or houses.get(house))
|
||
if _dict(row.get("significators")):
|
||
out[house] = row
|
||
return out
|
||
|
||
|
||
def _cusp_text(row: dict, language: str) -> str:
|
||
longitude = row.get("cusp_longitude")
|
||
if not isinstance(longitude, (int, float)) or isinstance(longitude, bool):
|
||
return "—"
|
||
degree = float(longitude) % 30.0
|
||
sign = row.get("sign") or ("Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio",
|
||
"Sagittarius", "Capricorn", "Aquarius", "Pisces")[int(float(longitude) % 360 // 30)]
|
||
return f"{sign_name(sign, language)} {degree:.4f}°"
|
||
|
||
|
||
def _promise_judgement(significators: dict, language: str) -> str:
|
||
if significators.get("A") not in (None, "", [], ()):
|
||
return ("宫头承诺线索清晰,可作为该主题的专项核对入口", "Clear cusp promise; a usable entry for this theme")[_lang(language)]
|
||
if any(significators.get(key) not in (None, "", [], ()) for key in ("B", "C")):
|
||
return ("宫头已有局部牵连,但不足以单独支撑结论", "Partial links; not enough on its own")[_lang(language)]
|
||
return ("宫头牵连偏弱,只作补充观察", "Weak links; supplementary only")[_lang(language)]
|
||
|
||
|
||
def _kp_section(packet: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
sensitive = _status("parameter_sensitive", language)
|
||
houses = _kp_houses(packet)
|
||
lines: list[str] = []
|
||
if houses:
|
||
lines.extend([
|
||
"### KP house view of the significators (Significations of the Houses)" if en
|
||
else "### KP 宫位显著星(宫位视角 / Significations of the Houses)",
|
||
"",
|
||
("The planet table above, read by house: which planets signify each house at the A (very strong) to D (weak) level. "
|
||
"It is a structural cross-check, not an event or timing verdict." if en else
|
||
"把前面的行星显著星表按宫位重排:每个宫由哪些行星在 A(很强)到 D(较弱)层级上牵连。只作结构核对,不单独生成事件或应期判断。"),
|
||
"",
|
||
*_table(("House", "Very strong (A)", "Strong (B)", "Normal (C)", "Weak (D)", "Status") if en
|
||
else ("宫位", "很强(A)", "强(B)", "一般(C)", "较弱(D)", "状态"),
|
||
[(str(house), *(_significator(row["significators"].get(key), language) for key in "ABCD"), sensitive)
|
||
for house, row in houses.items()]),
|
||
"",
|
||
"### KP Cuspal Promise (house-level judgement)" if en else "### KP Cuspal Promise(宫头承诺 / 宫位判断)",
|
||
"",
|
||
("Each cusp with its theme and the planets signifying it. A house whose A level is filled has a clear promise; "
|
||
"this is still not an event chain." if en else
|
||
"每个宫头连同主题与牵连它的行星。A 层有星的宫,宫头承诺较清晰;这仍不是事件链,也不单独给出应期。"),
|
||
"",
|
||
*_table(("House", "Cusp", "Theme", "A", "B", "C", "D", "Judgement", "Status") if en
|
||
else ("宫位", "宫头", "主题", "A", "B", "C", "D", "宫位判断", "状态"),
|
||
[(str(house), _cusp_text(row, language), HOUSE_THEMES[house][_lang(language)],
|
||
*(_significator(row["significators"].get(key), language) for key in "ABCD"),
|
||
_promise_judgement(row["significators"], language), sensitive)
|
||
for house, row in houses.items()]),
|
||
"",
|
||
])
|
||
for _domain, numbers, label, reader in KP_DOMAINS:
|
||
rows = [(str(house), *(_significator(houses[house]["significators"].get(key), language) for key in "ABCD"), sensitive)
|
||
for house in numbers if house in houses]
|
||
if not rows:
|
||
continue
|
||
suffix = " / ".join(str(number) for number in numbers)
|
||
lines.extend([
|
||
f"### KP {label[1].lower()} house check (houses {suffix})" if en else f"### KP {label[0]}宫位核对({suffix} 宫)",
|
||
"",
|
||
reader[_lang(language)],
|
||
"",
|
||
*_table(("House", "A", "B", "C", "D", "Status") if en else ("宫位", "A", "B", "C", "D", "状态"), rows),
|
||
"",
|
||
])
|
||
lines.extend(_dignity_section(packet, language))
|
||
lines.extend(_bhava_aspect_section(packet, language))
|
||
return lines
|
||
|
||
|
||
def _dignity_word(value: Any, language: str) -> str:
|
||
text = str(value or "").strip()
|
||
if not text:
|
||
return "—"
|
||
if "(" in text and text.endswith(")"):
|
||
chinese, english = text[:-1].split("(", 1)
|
||
return english.strip() if language == "en" else chinese.strip()
|
||
return text if language == "en" else DIGNITY_ZH.get(text, text)
|
||
|
||
|
||
def _dignity_section(packet: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
varga_full = _dict(_sheet(packet, "divisional_and_special_charts").get("varga_full"))
|
||
d1 = _dict(_sheet(packet, "d1_rasi_bhava").get("planets"))
|
||
rows = []
|
||
for label, key in DIGNITY_VARGAS:
|
||
if key is None:
|
||
values = [_dignity_word(_dict(d1.get(planet)).get("status"), language) for planet in PLANETS]
|
||
if all(value == "—" for value in values):
|
||
continue
|
||
else:
|
||
dignity = _dict(_dict(varga_full.get(key)).get("_dignity"))
|
||
if not dignity:
|
||
continue
|
||
values = [_dignity_word(dignity.get(planet), language) for planet in PLANETS]
|
||
rows.append((label, *values, _status("parameter_sensitive", language)))
|
||
if not rows:
|
||
return []
|
||
return [
|
||
"### Shodashvarga dignity matrix" if en else "### Shodashvarga 分盘尊贵矩阵",
|
||
"",
|
||
("Each planet's dignity in D1 and the sixteen-varga set, as the engine returns it. D1 carries the friend/enemy "
|
||
"gradient; the vargas carry exalted / own / debilitated / neutral only." if en else
|
||
"各行星在 D1 与十六分盘中的尊贵状态,取自引擎原值。D1 带友敌梯度;分盘只分入旺、自宫、落陷与中性。"),
|
||
"",
|
||
*_table(("Varga", *(planet_name(planet, language) for planet in PLANETS), "Status") if en
|
||
else ("分盘", *(planet_name(planet, language) for planet in PLANETS), "状态"), rows),
|
||
"",
|
||
]
|
||
|
||
|
||
def _bhava_aspect_section(packet: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
aspects = _dict(_sheet(packet, "advanced_systems").get("aspects"))
|
||
rows = []
|
||
for item in _list(aspects.get("house_aspects")):
|
||
item = _dict(item)
|
||
targets = _dict(item.get("aspects_to"))
|
||
for target in sorted(targets, key=lambda value: int(value) if str(value).isdigit() else 99):
|
||
detail = _dict(targets[target])
|
||
kind = str(detail.get("aspect_type") or "")
|
||
if en:
|
||
kind = kind.replace("宫相位", "th-house aspect")
|
||
rows.append((planet_name(item.get("planet"), language), str(detail.get("from_house") or item.get("house") or "—"),
|
||
str(target), kind or "—", _yes(bool(detail.get("is_special")), language),
|
||
_status("parameter_sensitive", language)))
|
||
if not rows:
|
||
return []
|
||
return [
|
||
"### Planet-to-Bhava aspects (Parashari)" if en else "### 行星对 Bhava 相位(Parashari 规则)",
|
||
"",
|
||
("Which houses each planet aspects under the Parashari house rule (7th for all; 4/8 Mars, 5/9 Jupiter, 3/10 Saturn). "
|
||
"Nodes are not included in this rule set." if en else
|
||
"按 Parashari 宫位相位规则列出每颗星照到的宫(各星 7 宫照;火星另照 4/8、木星 5/9、土星 3/10)。本规则不含交点。"),
|
||
"",
|
||
*_table(("Planet", "From house", "To house", "Aspect", "Special", "Status") if en
|
||
else ("行星", "来源宫", "目标宫", "相位类型", "特殊相位", "状态"), rows),
|
||
"",
|
||
]
|
||
|
||
|
||
def _level_lord(levels: dict, key: str, language: str) -> str:
|
||
lord = _dict(levels.get(key)).get("lord")
|
||
return planet_name(lord, language) if lord else "—"
|
||
|
||
|
||
def _sign_changes(previous: dict, current: dict, language: str) -> list[str]:
|
||
out = []
|
||
for planet in ("Jupiter", "Saturn", "Rahu", "Ketu"):
|
||
before = _dict(previous.get(planet)).get("sign")
|
||
after = _dict(current.get(planet)).get("sign")
|
||
if before and after and before != after:
|
||
if language == "en":
|
||
out.append(f"{planet_name(planet, language)} {sign_name(before, language)}→{sign_name(after, language)}")
|
||
else:
|
||
out.append(f"{planet_name(planet, language)}换座:{sign_name(before, language)}→{sign_name(after, language)}")
|
||
return out
|
||
|
||
|
||
def _monthly_section(packet: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
sensitive = _status("parameter_sensitive", language)
|
||
monthly = _dict(_sheet(packet, "advanced_systems").get("kp_monthly_report"))
|
||
months = [_dict(row) for row in _list(monthly.get("months")) if _dict(row).get("month")]
|
||
ephemeris = _dict(_sheet(packet, "timing_and_predictive_systems").get("three_year_predictive_ephemeris_pack"))
|
||
ephemeris_by_month = {str(_dict(row).get("month")): _dict(row) for row in _list(ephemeris.get("months"))}
|
||
slow_by_month = {month: _dict(row.get("slow_planets")) for month, row in ephemeris_by_month.items()}
|
||
lines: list[str] = []
|
||
if months:
|
||
lines.extend([
|
||
"### Three-year KP monthly support" if en else "### KP 三年流月支持",
|
||
"",
|
||
(f"{months[0]['month']} to {months[-1]['month']}, {len(months)} months. Each month: the five Vimshottari levels at the "
|
||
"month anchor (1st, local noon), the birth ruling planet, the cusps the month triggers and slow-planet sign changes. "
|
||
"A support layer, not an event forecast." if en else
|
||
f"{months[0]['month']} 至 {months[-1]['month']},共 {len(months)} 个月。每月列出月锚点(当月 1 日本地正午)的五层 "
|
||
"Vimshottari、出生时刻主宰星、当月触发的宫头与慢行星换座。只是支持层,不是事件预测。"),
|
||
"",
|
||
])
|
||
by_year: dict[str, list[dict]] = {}
|
||
for row in months:
|
||
by_year.setdefault(str(row["month"])[:4], []).append(row)
|
||
for year, rows in sorted(by_year.items()):
|
||
header = (("Month", "MD / AD / PD", "Sookshma / Prana", "Ruling planet", "Cusp trigger", "Slow planets", "Status") if en
|
||
else ("月份", "大运 / 小运 / 细运", "微运 / 息运", "主宰星", "宫头触发", "慢行星提示", "事业", "财务", "关系", "状态"))
|
||
body = []
|
||
for row in rows:
|
||
levels = _dict(_dict(row.get("vimshottari_five_levels")).get("levels"))
|
||
support = _dict(row.get("theme_support"))
|
||
domains = [_dict(support.get(key)) for key in ("career", "wealth", "relationship")]
|
||
ruling = next((item.get("ruling_planet_snapshot") for item in domains if item.get("ruling_planet_snapshot")), None)
|
||
trigger = sorted({house for item in domains for house in _list(item.get("active_houses"))})
|
||
slow = slow_by_month.get(str(row["month"])) or {
|
||
planet: _dict(_dict(_dict(row.get("monthly_transits")).get("planets")).get(planet))
|
||
for planet in ("Jupiter", "Saturn", "Rahu", "Ketu")}
|
||
if en:
|
||
changes = _sign_changes(slow, slow_by_month.get(_next_month(str(row["month"])), {}), language)
|
||
else: # the engine's own wording, as upstream: changes during the month, retrograde notes left out
|
||
signals = _list(_dict(ephemeris_by_month.get(str(row["month"]))).get("slow_planet_signals")) \
|
||
or _list(row.get("slow_planet_signals"))
|
||
changes = [str(item) for item in signals if item and "逆行强调" not in str(item)]
|
||
cells = [
|
||
str(row["month"]),
|
||
" / ".join(_level_lord(levels, key, language) for key in ("mahadasha", "antardasha", "pratyantardasha")),
|
||
" / ".join(_level_lord(levels, key, language) for key in ("sookshma", "prana")),
|
||
planet_name(ruling, language) if ruling else "—",
|
||
("、" if not en else ", ").join(f"{house}{'' if en else '宫'}" for house in trigger) or "—",
|
||
(";" if not en else "; ").join(changes) or "—",
|
||
]
|
||
if not en:
|
||
cells.extend(_md_cell(item.get("brief_summary")) or "—" for item in domains)
|
||
cells.append(sensitive)
|
||
body.append(tuple(cells))
|
||
lines.extend([f"#### {year} KP monthly support" if en else f"#### {year} KP 月度支持", "", *_table(header, body), ""])
|
||
highlights = _list(ephemeris.get("yearly_highlights")) or _list(monthly.get("yearly_highlights"))
|
||
highlight_rows = [(str(_dict(year).get("year")), [_dict(item) for item in _list(_dict(year).get("months"))])
|
||
for year in highlights if _dict(year).get("year")]
|
||
if highlight_rows:
|
||
lines.extend([
|
||
"### Ephemeris key-month index" if en else "### 星历重点月份索引",
|
||
"",
|
||
("Months where a slow planet changes sign or turns retrograde. They are the months to read first, not months "
|
||
"where an event is certain." if en else
|
||
"慢行星换座或逆行强调的月份。这些是更值得优先阅读的月份,不代表具体事件一定发生。"),
|
||
"",
|
||
])
|
||
for year, items in highlight_rows:
|
||
body = []
|
||
for item in items:
|
||
if en:
|
||
month = str(item.get("month") or "")
|
||
reason = "; ".join(_sign_changes(slow_by_month.get(month, {}), slow_by_month.get(_next_month(month), {}), language)) \
|
||
or "slow-planet retrograde emphasis"
|
||
else:
|
||
reason = _md_cell(item.get("reason")) or "—"
|
||
body.append((str(item.get("month") or "—"), reason, _status(item.get("status") or "parameter_sensitive", language)))
|
||
lines.extend([f"#### {year} key months" if en else f"#### {year} 年重点月份索引", "",
|
||
*_table(("Month", "Why read it first", "Status") if en else ("月份", "为什么优先看", "状态"), body), ""])
|
||
lines.extend(_annual_extra_points(packet, language))
|
||
return lines
|
||
|
||
|
||
def _next_month(month: str) -> str:
|
||
try:
|
||
year, number = (int(part) for part in month.split("-", 1))
|
||
except ValueError:
|
||
return ""
|
||
return f"{year + 1:04d}-01" if number == 12 else f"{year:04d}-{number + 1:02d}"
|
||
|
||
|
||
def _annual_extra_points(packet: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
timing = _sheet(packet, "timing_and_predictive_systems")
|
||
years = _dict(_dict(timing.get("annual_tajika_series")).get("years"))
|
||
packs = [(str(year), _dict(pack)) for year, pack in sorted(years.items())] or (
|
||
[(str(_dict(_dict(timing.get("annual_tajika_pack")).get("profile")).get("target_year") or "—"),
|
||
_dict(timing.get("annual_tajika_pack")))] if timing.get("annual_tajika_pack") else [])
|
||
rows = []
|
||
for year, pack in packs:
|
||
yogas = _dict(pack.get("tajika_yogas"))
|
||
interactions = [item for item in _list(_dict(yogas.get("data")).get("interactions")) if _dict(item).get("within_deeptamsa")]
|
||
if yogas:
|
||
rows.append((year, "Tajika Yogas",
|
||
f"{len(interactions)} pairs within Deeptamsa" if en else f"{len(interactions)} 对行星在 Deeptamsa 容许度内",
|
||
_status(yogas.get("status"), language)))
|
||
named = _dict(pack.get("tajika_named_yogas"))
|
||
if named:
|
||
supported = _list(_dict(named.get("data")).get("supported_named_yogas"))
|
||
rows.append((year, "Tajika named yogas" if en else "Tajika 命名瑜伽",
|
||
f"{len(supported)} named yogas supported" if en else f"支持 {len(supported)} 种命名瑜伽",
|
||
_status(named.get("status"), language)))
|
||
sahams = _dict(pack.get("sahams"))
|
||
if sahams:
|
||
count = sum(1 for key, value in _dict(sahams.get("data")).items() if key.endswith("_saham") and isinstance(value, dict))
|
||
rows.append((year, "Sahams", f"{count} points" if en else f"{count} 个点位", _status(sahams.get("status"), language)))
|
||
raman = _dict(pack.get("raman_sahams"))
|
||
if raman:
|
||
rows.append((year, "Raman Sahams", f"{len(_list(raman.get('rows')))} points" if en else f"{len(_list(raman.get('rows')))} 个点位",
|
||
_status(raman.get("status"), language)))
|
||
if not rows:
|
||
return []
|
||
return [
|
||
"### Annual extra points index" if en else "### 年度附加点索引",
|
||
"",
|
||
("Where each year's Tajika interactions and Saham points stand. The points themselves are in the annual chapters." if en
|
||
else "各年度 Tajika 交互与 Saham 点位的完成情况。点位本身在年度章节。"),
|
||
"",
|
||
*_table(("Year", "Item", "Count", "Status") if en else ("年份", "项目", "数量", "状态"), rows),
|
||
"",
|
||
]
|
||
|
||
|
||
def _birth_moment(packet: dict) -> datetime | None:
|
||
info = _dict(packet.get("birth_info"))
|
||
try:
|
||
if info.get("date"):
|
||
day = datetime.strptime(str(info["date"])[:10], "%Y-%m-%d")
|
||
else:
|
||
day = datetime(int(info["year"]), int(info["month"]), int(info["day"]))
|
||
if info.get("hour") is None and info.get("time"):
|
||
hour, minute = (int(part) for part in str(info["time"]).split(":")[:2])
|
||
else:
|
||
hour, minute = int(info.get("hour") or 0), int(info.get("minute") or 0)
|
||
return day.replace(hour=hour, minute=minute)
|
||
except (KeyError, TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def _tz_hours(packet: dict) -> float:
|
||
"""``birth_info.tz`` is ``UTC+8.0`` in the report packet; the profile carries ``+08:00``."""
|
||
info = _dict(packet.get("birth_info"))
|
||
raw = info.get("tz") if info.get("tz") is not None else info.get("timezone")
|
||
if isinstance(raw, (int, float)) and not isinstance(raw, bool):
|
||
return float(raw)
|
||
text = str(raw or "").upper().replace("UTC", "").replace("GMT", "").strip()
|
||
if not text:
|
||
text = str(_dict(_dict(packet.get("calculation_profile")).get("effective_settings")).get("timezone_offset") or "")
|
||
if ":" in text:
|
||
sign = -1.0 if text.startswith("-") else 1.0
|
||
hours, minutes = text.lstrip("+-").split(":", 1)
|
||
return sign * (int(hours) + int(minutes) / 60.0)
|
||
return float(text)
|
||
|
||
|
||
def three_minute_sensitivity(packet: dict) -> dict:
|
||
"""Upstream's ±1-minute recast matrix (yinduzhanxing ``_build_birth_time_sensitivity``)."""
|
||
info = _dict(packet.get("birth_info"))
|
||
center = _birth_moment(packet)
|
||
window = {}
|
||
if center is not None:
|
||
window = {"start": (center - timedelta(minutes=1)).strftime("%H:%M"), "center": center.strftime("%H:%M"),
|
||
"end": (center + timedelta(minutes=1)).strftime("%H:%M")}
|
||
base = {
|
||
"status": "blocked",
|
||
"window": window,
|
||
"approval_gate": {"status": "blocked", "can_enter_pending_approval": False,
|
||
"blocked_reasons": ["review_not_materialized"], "required_next_step": "record_rectification_review"},
|
||
"candidate_micro_compare": {"status": "blocked", "candidate_minutes": [], "minute_results": []},
|
||
}
|
||
if center is None:
|
||
return base
|
||
try:
|
||
try:
|
||
from scripts.flexible_birth_time_profile import build_flexible_birth_time_profile_from_window
|
||
except ModuleNotFoundError: # pragma: no cover
|
||
from flexible_birth_time_profile import build_flexible_birth_time_profile_from_window
|
||
profile_info = _dict(_dict(packet.get("calculation_profile")).get("effective_settings"))
|
||
profile = build_flexible_birth_time_profile_from_window(
|
||
birth_date=center.strftime("%Y-%m-%d"),
|
||
start_time=window["start"],
|
||
end_time=window["end"],
|
||
candidate_times=[window["start"], window["center"], window["end"]],
|
||
lat=float(info["lat"]),
|
||
lon=float(info["lon"]),
|
||
tz=_tz_hours(packet),
|
||
ayanamsa=str(profile_info.get("ayanamsa") or info.get("ayanamsa_name") or "lahiri"),
|
||
node_mode=str(profile_info.get("node_mode") or info.get("node_mode") or "mean"),
|
||
source_reference={"review_id": f"rectification-review://report-window/{center.strftime('%Y%m%d-%H%M')}",
|
||
"status": "review_required"},
|
||
)
|
||
except Exception: # noqa: BLE001 - the chapter shows the matrix as not run
|
||
return base
|
||
return {**base, "status": "candidate_window_only", "profile": profile}
|
||
|
||
|
||
def _rect_section(packet: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
provenance = _dict(packet.get("birth_provenance"))
|
||
precision = _dict(packet.get("timing_precision_contract"))
|
||
replay = _dict(packet.get("event_replay"))
|
||
contract = _dict(packet.get("rectification_evidence_contract"))
|
||
sensitivity = three_minute_sensitivity(packet)
|
||
lines = [
|
||
"### Birth data source and uncertainty" if en else "### 出生资料来源与不确定范围",
|
||
"",
|
||
f"- {'Place label' if en else '地点标签'}{_colon(language)}{_word(provenance.get('birthplace_label', 'not_recorded'), language)}",
|
||
f"- {'Coordinate precision' if en else '坐标精度'}{_colon(language)}{_word(provenance.get('coordinate_precision', 'unverified_user_coordinates'), language)}",
|
||
f"- {'Coordinate source' if en else '坐标来源'}{_colon(language)}{_word(provenance.get('coordinate_source', 'not_recorded'), language)}",
|
||
f"- {'Birth time source' if en else '出生时间来源'}{_colon(language)}{_word(provenance.get('time_source', 'not_recorded'), language)}",
|
||
f"- {'Time uncertainty (minutes)' if en else '时间不确定范围(分钟)'}{_colon(language)}{_word(provenance.get('uncertainty_minutes', 'not_recorded'), language)}",
|
||
"",
|
||
"### Timing precision contract" if en else "### 时间精度合同",
|
||
"",
|
||
("Exact days are not given; weeks and months give candidate windows only; a year's themes come from that year's own "
|
||
"solar return. Every level needs real event replay before its wording can get stronger." if en else
|
||
"日级精确应期不可用;周级、月级只能给候选观察窗口;年度主题必须来自对应年份独立返照。各层级都要经过现实事件回放才能提高断言强度。"),
|
||
"",
|
||
]
|
||
tiers = _dict(precision.get("tiers"))
|
||
tier_names = {"day": ("日", "day"), "week": ("周", "week"), "month": ("月", "month"), "year": ("年", "year")}
|
||
lines.extend(_table(("Granularity", "Status", "Allowed", "Not allowed") if en else ("粒度", "状态", "允许结论", "禁止结论"),
|
||
[(tier_names[tier][_lang(language)], _status(_dict(tiers.get(tier)).get("status") or "blocked", language),
|
||
_words(_dict(tiers.get(tier)).get("allowed_claims"), language),
|
||
_words(_dict(tiers.get(tier)).get("prohibited_claims"), language))
|
||
for tier in ("day", "week", "month", "year")]))
|
||
events = [_dict(item) for item in _list(replay.get("events")) if isinstance(item, dict)]
|
||
lines.extend([
|
||
"",
|
||
"### Event replay check" if en else "### 事件回放校验",
|
||
"",
|
||
f"- {'Status' if en else '状态'}{_colon(language)}{_status(replay.get('status') or 'blocked', language)}",
|
||
f"- {'Reason' if en else '原因'}{_colon(language)}{_word(replay.get('reason') or 'user_history_not_supplied', language) if replay.get('reason') else _clean(replay.get('boundary'), language)}",
|
||
f"- {'Replay lanes' if en else '校验轨道'}{_colon(language)}{_words(replay.get('validation_lanes'), language)}",
|
||
f"- {'Fields each event needs' if en else '每条事件需提供'}{_colon(language)}{_words(replay.get('required_event_fields'), language)}",
|
||
])
|
||
if events:
|
||
lines.extend(["", *_table(("Year / date", "Domain", "Event", "Outcome") if en else ("年份/日期", "领域", "事件", "结果/观察"),
|
||
[(_md_cell(item.get("year_or_date")) or "—", _word(item.get("domain"), language),
|
||
_md_cell(item.get("event_description")) or "—", _md_cell(item.get("outcome_or_observation")) or "—")
|
||
for item in events])])
|
||
blind = _dict(replay.get("blind_holdout_policy"))
|
||
if blind:
|
||
lines.extend([
|
||
"",
|
||
"#### Blind replay and no-tuning boundary" if en else "#### 盲回放与防调参边界",
|
||
"",
|
||
f"- {'Status' if en else '状态'}{_colon(language)}{_word(blind.get('status'), language)}",
|
||
f"- {'Labels frozen first' if en else '标签先冻结'}{_colon(language)}{_yes(blind.get('freeze_user_labels_before_comparison'), language)}",
|
||
f"- {'No tuning the birth time or parameters from the same events' if en else '禁止用同一批事件反调出生时间或参数'}{_colon(language)}{_yes(blind.get('forbid_birth_time_or_parameter_tuning_from_same_events'), language)}",
|
||
("- Parent and family events may be compared against D12 only to tell candidates apart; D12 cannot pick a minute by itself."
|
||
if en else "- 父母/家庭事件只能拿来和 D12 比较候选差异;D12 不能单独选出分钟。"),
|
||
])
|
||
lines.extend(_verdict_section(packet, sensitivity, language))
|
||
lines.extend(_sensitivity_section(sensitivity, events, language))
|
||
lines.extend(_domain_contract_section(contract, language))
|
||
return lines
|
||
|
||
|
||
def _verdict_section(packet: dict, sensitivity: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
matrix = _dict(packet.get("prediction_verdict_matrix"))
|
||
if not matrix:
|
||
try:
|
||
from scripts.prediction_verdict_matrix import build_prediction_verdict_matrix
|
||
except ModuleNotFoundError: # pragma: no cover
|
||
from prediction_verdict_matrix import build_prediction_verdict_matrix
|
||
view = dict(packet)
|
||
if _dict(packet.get("birth_time_sensitivity")).get("status") != "candidate_window_only":
|
||
view["birth_time_sensitivity"] = {"status": sensitivity.get("status")}
|
||
matrix = build_prediction_verdict_matrix(view)
|
||
rows = []
|
||
for row in _list(matrix.get("rows")):
|
||
row = _dict(row)
|
||
selector = _dict(row.get("selector"))
|
||
rows.append((_word(row.get("domain"), language), str(row.get("event_count", 0)),
|
||
_status(row.get("evidence_label"), language) if row.get("evidence_label") in ("blocked", "parameter_sensitive")
|
||
else _word(row.get("evidence_label"), language),
|
||
_word(row.get("claim_cap"), language), _words(row.get("blockers"), language, ";" if not en else "; "),
|
||
" / ".join(_word(item, language) if item in WORDS else str(item) for item in _list(selector.get("primary_dashas"))) or "—",
|
||
" / ".join(_word(item, language) if item in WORDS else str(item) for item in _list(selector.get("supporting_layers"))) or "—"))
|
||
return [
|
||
"",
|
||
"### Prediction verdict matrix" if en else "### 断事推运判决矩阵",
|
||
"",
|
||
f"- {'Status' if en else '状态'}{_colon(language)}{_status(matrix.get('status') or 'blocked', language)}",
|
||
f"- {'Strength filter' if en else '力量过滤'}{_colon(language)}{_status(_dict(matrix.get('strength_filter_gate')).get('status') or 'blocked', language)}",
|
||
f"- {'Parameter sensitivity' if en else '参数敏感'}{_colon(language)}{_word(_dict(matrix.get('parameter_sensitivity')).get('status'), language) if _dict(matrix.get('parameter_sensitivity')).get('status') == 'stable' else _status(_dict(matrix.get('parameter_sensitivity')).get('status') or 'blocked', language)}",
|
||
("- The matrix only adjudicates evidence already in this report; without event replay it cannot claim exact dates or a confirmed result."
|
||
if en else "- 本矩阵只裁决报告里已有的证据;没有事件回放时,不能据此给出确定日期或已证实的结果。"),
|
||
"",
|
||
*_table(("Domain", "Events", "Evidence", "Claim cap", "Blockers", "Primary dashas", "Supporting layers") if en
|
||
else ("领域", "事件数", "证据等级", "措辞上限", "阻断项", "主大运", "支撑层"), rows),
|
||
]
|
||
|
||
|
||
def _sensitivity_value(value: Any, language: str) -> str:
|
||
if isinstance(value, dict):
|
||
parts = []
|
||
for minute, item in value.items():
|
||
if isinstance(item, dict):
|
||
cusps = []
|
||
for house, cusp in item.items():
|
||
cusp = _dict(cusp)
|
||
if language == "en":
|
||
cusps.append(f"H{house.replace('house_', '')} {sign_name(cusp.get('sign'), language)} "
|
||
f"(star {planet_name(cusp.get('nakshatra_lord'), language)}, sub {planet_name(cusp.get('sub_lord'), language)})")
|
||
else:
|
||
cusps.append(f"{house.replace('house_', '')}宫 {sign_name(cusp.get('sign'), language)}"
|
||
f"(星宿主{planet_name(cusp.get('nakshatra_lord'), language)},分主{planet_name(cusp.get('sub_lord'), language)})")
|
||
parts.append(f"{minute}{_colon(language)}{','.join(cusps) if language != 'en' else ', '.join(cusps)}")
|
||
else:
|
||
parts.append(f"{minute} {sign_name(item, language)}")
|
||
return ";".join(parts) if language != "en" else "; ".join(parts)
|
||
return sign_name(value, language) if value not in (None, "") else "—"
|
||
|
||
|
||
def _layer_label(key: str, language: str) -> str:
|
||
"""Registry layer keys as words (``D1.house_4``, ``KP.cusp_observation`` are packet keys, not prose)."""
|
||
en = language == "en"
|
||
if key == "KP.cusp_observation":
|
||
return "KP cusps 1/4/7/10" if en else "KP 宫头(1/4/7/10)"
|
||
if key.endswith(".ascendant"):
|
||
return f"{key.split('.')[0]} {'ascendant' if en else '上升'}"
|
||
if key.startswith("arudha."):
|
||
return key.split(".", 1)[1]
|
||
if ".house_" in key:
|
||
chart, house = key.split(".house_", 1)
|
||
return f"{chart} house {house}" if en else f"{chart} 第{house}宫"
|
||
return _clean(key, language)
|
||
|
||
|
||
def _sensitivity_section(sensitivity: dict, events: list[dict], language: str) -> list[str]:
|
||
en = language == "en"
|
||
window = _dict(sensitivity.get("window"))
|
||
gate = _dict(sensitivity.get("approval_gate"))
|
||
profile = _dict(sensitivity.get("profile"))
|
||
stable = _dict(profile.get("stable_evidence"))
|
||
sensitive = _dict(profile.get("sensitive_evidence"))
|
||
rows = []
|
||
for key in SENSITIVITY_KEYS:
|
||
if key in stable:
|
||
value, kind = stable[key], "stable_across_window"
|
||
elif key in sensitive:
|
||
value, kind = sensitive[key], "minute_sensitive"
|
||
else:
|
||
value, kind = None, "blocked"
|
||
if key == "D12.ascendant":
|
||
boundary = ("Only with dated parent/family events; never picks a minute alone." if en
|
||
else "仅在有日期化父母/家庭事件时参与候选比较;不得单独选择分钟。")
|
||
elif key == "KP.cusp_observation":
|
||
boundary = ("Local observation of cusps 1/4/7/10 (sign / star lord / sub lord); exact-cusp oracle not closed." if en
|
||
else "1/4/7/10 宫头的本地观察(星座/星宿主/分主);精确宫头外部核对未闭合。")
|
||
else:
|
||
boundary = ("Raw structure for comparing candidates; cannot approve a birth minute alone." if en
|
||
else "可作为候选比较的原始结构,不可单独批准出生分钟。")
|
||
rows.append((_layer_label(key, language), _sensitivity_value(value, language) if value is not None else _status("blocked", language),
|
||
_word(kind, language) if kind != "blocked" else _status("blocked", language), boundary))
|
||
minute_status = "blocked"
|
||
lines = [
|
||
"",
|
||
"### Birth-time sensitivity matrix" if en else "### 生时敏感性矩阵",
|
||
"",
|
||
f"- {'Status' if en else '状态'}{_colon(language)}{_status(sensitivity.get('status'), language)}",
|
||
f"- {'Candidate minutes' if en else '候选窗口'}{_colon(language)}{window.get('start', '—')} / {window.get('center', '—')} / {window.get('end', '—')}",
|
||
f"- {'Review to approval gate' if en else '复核到批准门槛'}{_colon(language)}{_status(gate.get('status') or 'blocked', language)}",
|
||
f"- {'Can enter pending approval' if en else '可进入待批准'}{_colon(language)}{_yes(gate.get('can_enter_pending_approval', False), language)}",
|
||
f"- {'Held back because' if en else '阻断原因'}{_colon(language)}{_words(gate.get('blocked_reasons'), language)}",
|
||
f"- {'Next step' if en else '下一步'}{_colon(language)}{_word(gate.get('required_next_step'), language)}",
|
||
"",
|
||
*_table(("Layer", "Three-minute result", "Type", "Boundary") if en else ("校时层", "三分钟结果", "类型", "使用边界"), rows),
|
||
"",
|
||
("- This is a local comparison of the minute before and after, not a rectification result. It cannot replace the chart, "
|
||
"pick a winning minute or change the chart identity." if en else
|
||
"- 这只是出生分钟前后各一分钟的本地比较,不是校时结果;不能替换当前命盘、选出胜出分钟或改变命盘身份。"),
|
||
"",
|
||
"#### Rectification appendix: minute ranking" if en else "#### 校时附录:分钟排行表",
|
||
"",
|
||
f"- {'Status' if en else '状态'}{_colon(language)}{_status(minute_status, language)}",
|
||
(("- Events were supplied, but the report path does not score minutes; run the rectification tool for a ranking."
|
||
if en else "- 已提供事件,但报告路径不给分钟打分;分钟排行请用出生时间校正工具。") if events else
|
||
("- Minute comparison is packaged for the report but is not run until past events are supplied for replay."
|
||
if en else "- 分钟比较已打包进报告,但在提供过往事件做回放之前保持阻塞。")),
|
||
]
|
||
return lines
|
||
|
||
|
||
def _domain_contract_section(contract: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
domains = [_dict(item) for item in _list(contract.get("domains")) if isinstance(item, dict)]
|
||
rules = _dict(contract.get("global_rules"))
|
||
lines = ["", "### Rectification evidence domain contract" if en else "### 校时证据领域合同", ""]
|
||
if not domains:
|
||
return lines + [f"- {'Status' if en else '状态'}{_colon(language)}{_status('blocked', language)}", ""]
|
||
lines.extend([
|
||
("Maps independently checkable life events to the evidence used to compare candidate minutes. It does not pick a "
|
||
"birth minute, establish medical facts or make event timing more precise than the precision contract allows."
|
||
if en else "把可独立核验的人生事件对应到候选分钟比较用的证据层。它不选出生分钟、不确立医疗事实,也不把应期精度提到时间精度合同之上。"),
|
||
"",
|
||
(f"- Global gate: at least {rules.get('minimum_independent_support_layers', '—')} independent supporting layers; labels frozen "
|
||
f"first = {_yes(rules.get('freeze_event_labels_before_candidate_comparison'), language)}; D60 decision power = "
|
||
f"{_word(rules.get('d60_decision_authority'), language)}; D40/D45 = {_word(rules.get('d40_d45_authority'), language)}."
|
||
if en else
|
||
f"- 全局门:至少 {rules.get('minimum_independent_support_layers', '—')} 个独立支持层;标签先冻结={_yes(rules.get('freeze_event_labels_before_candidate_comparison'), language)};"
|
||
f"D60 决策权={_word(rules.get('d60_decision_authority'), language)};D40/D45={_word(rules.get('d40_d45_authority'), language)}。"),
|
||
"",
|
||
])
|
||
rows = []
|
||
for item in domains:
|
||
label = DOMAIN_EN.get(str(item.get("domain_id")), _clean(item.get("domain_id"), language)) if en else (_md_cell(item.get("label")) or "—")
|
||
structural = ("、" if not en else ", ").join(_layer_label(str(layer), language) for layer in _list(item.get("structural_layers"))) or "—"
|
||
timing = ("、" if not en else ", ").join(_word(layer, language) if layer in WORDS else str(layer)
|
||
for layer in _list(item.get("timing_layers"))) or "—"
|
||
policy = item.get("special_policy")
|
||
policy_text = (_clean(policy, language) if policy else ("Global boundary" if en else "遵循全局边界"))
|
||
row = [label, structural, timing]
|
||
if not en:
|
||
row.append(";".join(_md_cell(example) for example in _list(item.get("objective_event_examples"))) or "—")
|
||
row.extend([str(item.get("minimum_cross_support", "—")), policy_text])
|
||
rows.append(tuple(row))
|
||
header = (("Event domain", "Structural layers", "Timing layers", "Minimum cross-support", "Special boundary") if en
|
||
else ("事件领域", "结构层", "时间/事件层", "可核验事实示例", "最低交叉", "专项边界"))
|
||
return lines + [*_table(header, rows), ""]
|
||
|
||
|
||
def render_upstream_sync_chapters(packet: dict, language: str) -> str:
|
||
language = "en" if language == "en" else "zh"
|
||
if not isinstance(packet, dict):
|
||
return ""
|
||
out: list[str] = []
|
||
kp = _kp_section(packet, language)
|
||
if kp:
|
||
out.extend([f"## {KP_HEADING[language]}", "", *kp])
|
||
monthly = _monthly_section(packet, language)
|
||
if monthly:
|
||
out.extend([f"## {MONTHLY_HEADING[language]}", "", *monthly])
|
||
out.extend([f"## {RECT_HEADING[language]}", "", *_rect_section(packet, language)])
|
||
return "\n".join(out).rstrip() + "\n"
|