产品负责人 2026-11-08:用户看到的报告文字不出现「联网 / 外部资料 / 外部核验 / MEVG / 真实案例 / Real Case / Global Web」,后台记录保留。 - 个人报告写作提示词:依据与边界改为「没有用户经历可对照」,置信度封顶 B 不变 - 年报:技法执行情况跳过 MEVG / Real Case 两行;没有纳入的删掉对应条目 - 合盘:技法执行情况删掉「外部资料与真实案例核对:未进行」整行 - 产品导出审计章节:这两行不再渲染,证据边界同类一句删除,packet 原样保留 - 质量门:只要求 Functional Benefic/Malefic 渲染;新增后台检查,要求 packet 仍带这两行且它们没有出现在 markdown 里 Tests: 新增 frontend/tests/report-no-web-wording-20261109.test.ts(4)与 tests/test_report_no_web_wording_20261109.py(3);更新年报、合盘、审计章节、 质量门既有断言(原值/新值/原因已注明)。 Co-Authored-By: Grok (agent loop)
464 lines
25 KiB
Python
464 lines
25 KiB
Python
"""Technique audit, method differences, evidence boundaries and the D1–D60 ledger.
|
||
|
||
产品 2026-11-07 占星师方案 item 10: the product export (``render_full_data_markdown``)
|
||
keeps what ``render_pl9_markdown`` has and the astrologer's quality gate asks
|
||
for: the full Technique Audit Table (with the Functional Benefic/Malefic row),
|
||
the parameter and method differences, the evidence boundaries, and the
|
||
60-division ledger (20 formal traditional divisions + 40 research D-N rows).
|
||
|
||
产品 2026-11-08(BUG-1810):用户可见文字不出现「联网 / 外部资料 / 外部核验 /
|
||
MEVG / 真实案例」这类说法。审计行与证据边界里的外部核验字样不再渲染;后台
|
||
packet 的 ``technique_audit_table`` 原样保留,质量门改为检查后台记录仍在。
|
||
|
||
Everything is read from the packet. Statuses are written as words, never as
|
||
internal codes, so the ordinary projection keeps the chapter.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any
|
||
|
||
try:
|
||
from scripts.report_render.text import SIGNS_CN, _md_cell
|
||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||
from report_render.text import SIGNS_CN, _md_cell
|
||
|
||
AUDIT_HEADING = {"zh": "技法审计与证据边界", "en": "Technique Audit and Evidence Boundaries"}
|
||
AUDIT_TABLE_HEADING = {"zh": "完整 Technique Audit Table", "en": "Full Technique Audit Table"}
|
||
LEDGER_HEADING = {
|
||
"zh": "D1–D60 完整原始分盘账本(20 传统 + 40 研究型 D-N)",
|
||
"en": "D1-D60 Complete Raw Divisional Ledger (20 formal + 40 research D-N)",
|
||
}
|
||
REQUIRED_ROWS = ("Functional Benefic/Malefic",)
|
||
# 原值:这三行都渲染,缺哪行就补哪行
|
||
# 新值:后两行不再渲染(后台 packet 保留);原因:产品负责人 2026-11-08:用户可见文字不出现联网/外部核验字样
|
||
HIDDEN_TECHNIQUES = ("MEVG / Global Web Evidence", "Real Case Calibration")
|
||
BODIES = ("Ascendant", "Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
|
||
PLANETS = BODIES[1:]
|
||
PLANET_ZH = {
|
||
"Ascendant": "上升", "Sun": "太阳", "Moon": "月亮", "Mars": "火星", "Mercury": "水星",
|
||
"Jupiter": "木星", "Venus": "金星", "Saturn": "土星", "Rahu": "罗睺", "Ketu": "计都",
|
||
}
|
||
NATURAL_BENEFICS = {"Jupiter", "Venus", "Mercury", "Moon"}
|
||
STATUS = {
|
||
"used": ("已调用", "in use"),
|
||
"executed": ("已执行", "Ran"),
|
||
# Engine status words that are also packet keys; written as reader words (2026-11-07).
|
||
"run": ("已执行", "Ran"),
|
||
"not_run": ("未执行", "Not run"),
|
||
"not run": ("未执行", "Not run"),
|
||
"available": ("可用", "available"),
|
||
"verified": ("已核验", "verified"),
|
||
"blocked": ("阻塞", "Not run"),
|
||
"error": ("阻塞", "Not run"),
|
||
"parameter_sensitive": ("参数敏感", "parameter-sensitive"),
|
||
"partial_verified": ("部分核验", "partly verified"),
|
||
"partial": ("部分可用", "partial"),
|
||
"not_applicable": ("不适用", "not applicable"),
|
||
"conflict": ("方法冲突", "method conflict"),
|
||
"candidate_window_only": ("仅候选窗口", "candidate window only"),
|
||
}
|
||
# Words the ordinary projection treats as engine leakage; the chapter says them another way.
|
||
LEAK_WORDS = (
|
||
(re.compile(r"\bparity\b", re.I), ("对拍", "cross-check")),
|
||
(re.compile(r"\bproducer\b", re.I), ("来源", "source")),
|
||
(re.compile(r"\bblocked\b", re.I), ("阻塞", "Not run")),
|
||
(re.compile(r"\bexecuted\b", re.I), ("已执行", "Ran")),
|
||
(re.compile(r"\bMEVG\b"), ("MEVG", "MEVG")),
|
||
)
|
||
SNAKE = re.compile(r"\b([A-Za-z][A-Za-z0-9]*)(?:_([A-Za-z0-9]+))+\b")
|
||
PY_LIST = re.compile(r"\[([^\[\]{}]*)\]")
|
||
PY_DICT = re.compile(r"\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}")
|
||
|
||
|
||
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 _status(value: Any, language: str) -> str:
|
||
key = str(value or "").strip().lower()
|
||
label = STATUS.get(key)
|
||
if label:
|
||
return label[_lang(language)]
|
||
return _clean(key or ("未声明" if language != "en" else "not declared"), language)
|
||
|
||
|
||
def _planet(name: Any, language: str) -> str:
|
||
text = str(name or "")
|
||
return text if language == "en" else PLANET_ZH.get(text, text)
|
||
|
||
|
||
def _sign(name: Any, language: str) -> str:
|
||
text = str(name or "")
|
||
return text if language == "en" else SIGNS_CN.get(text, text)
|
||
|
||
|
||
def _names(values: Any, language: str) -> str:
|
||
if not isinstance(values, list) or not values:
|
||
return "—"
|
||
return ("、" if language != "en" else ", ").join(_planet(item, language) for item in values)
|
||
|
||
|
||
HAN = re.compile(r"[\u3400-\u9fff]")
|
||
|
||
|
||
def _clean(text: Any, language: str) -> str:
|
||
"""Engine notes, made readable: no Python reprs, no snake_case keys, no leak words."""
|
||
out = str(text or "")
|
||
if language == "en" and HAN.search(out):
|
||
# The engine wrote this note in Chinese; the English edition must not carry Han text.
|
||
return "Engine note in the Chinese edition."
|
||
out = PY_DICT.sub("…", out)
|
||
out = PY_LIST.sub(lambda match: ("、" if language != "en" else ", ").join(
|
||
part.strip().strip("'\"") for part in match.group(1).split(",") if part.strip()
|
||
) or "—", out)
|
||
out = SNAKE.sub(lambda match: match.group(0).replace("_", " "), out)
|
||
for pattern, (zh, en) in LEAK_WORDS:
|
||
out = pattern.sub(en if language == "en" else zh, out)
|
||
if language != "en":
|
||
out = _quote_glue(out)
|
||
return _md_cell(out.strip()) or "—"
|
||
|
||
|
||
GLUE_AFTER_HAN = re.compile(r"(?<=[\u4e00-\u9fff])([ \t]*)((?:in|and|or|from|planet|lord)\b[\w\-/.]*)", re.I)
|
||
GLUE_BEFORE_HAN = re.compile(r"(?<![`\w])([\w\-/.]*\b(?:in|and|or|from|planet|lord))([ \t]*)(?=[\u4e00-\u9fff])", re.I)
|
||
|
||
|
||
def _quote_glue(text: str) -> str:
|
||
"""An English identifier right next to Han text reads as broken prose; quote it as code."""
|
||
text = GLUE_AFTER_HAN.sub(lambda m: f"{m.group(1)}`{m.group(2)}`", text)
|
||
return GLUE_BEFORE_HAN.sub(lambda m: f"`{m.group(1)}`{m.group(2)}", text)
|
||
|
||
|
||
def _source(value: Any, language: str) -> str:
|
||
text = str(value or "")
|
||
if text.startswith("references/"):
|
||
return f"`{text.split()[0]}`"
|
||
return "本地计算" if language != "en" else "local calculation"
|
||
|
||
|
||
def _functional_note(functional: dict, language: str) -> str:
|
||
benefic = _names(functional.get("functional_benefics"), language)
|
||
malefic = _names(functional.get("functional_malefics"), language)
|
||
neutral = _names(functional.get("functional_neutrals"), language)
|
||
yogakaraka = _names(functional.get("yogakarakas"), language)
|
||
conflicts = _conflicts(functional)
|
||
if language == "en":
|
||
tail = "; ".join(f"{row[0]} natural {row[1]} / functional {row[2]}" for row in conflicts) or "none"
|
||
return (f"Functional benefics {benefic}; functional malefics {malefic}; neutral {neutral}; Yogakaraka {yogakaraka}. "
|
||
f"Natural vs functional conflicts: {tail}. A conflicting planet lowers the confidence of the statement it supports.")
|
||
word = {"benefic": "吉", "malefic": "凶"}
|
||
tail = ";".join(f"{_planet(row[0], language)}(自然{word[row[1]]}、功能{word[row[2]]})" for row in conflicts) or "无"
|
||
return (f"功能吉星 {benefic};功能凶星 {malefic};中性 {neutral};Yogakaraka {yogakaraka}。"
|
||
f"自然与功能冲突:{tail}。用到冲突星的结论降一级置信度。")
|
||
|
||
|
||
def _conflicts(functional: dict) -> list[tuple[str, str, str]]:
|
||
benefic = set(functional.get("functional_benefics") or []) | set(functional.get("yogakarakas") or [])
|
||
malefic = set(functional.get("functional_malefics") or [])
|
||
rows = []
|
||
for planet in PLANETS:
|
||
if planet in ("Rahu", "Ketu"):
|
||
continue
|
||
natural = "benefic" if planet in NATURAL_BENEFICS else "malefic"
|
||
role = "benefic" if planet in benefic else "malefic" if planet in malefic else None
|
||
if role and role != natural:
|
||
rows.append((planet, natural, role))
|
||
return rows
|
||
|
||
|
||
def _dual_dasha_note(timing: dict, language: str) -> str:
|
||
vim = _dict(_dict(timing.get("dasha")).get("current_dasha"))
|
||
nar = _dict(_dict(_dict(timing.get("narayana_dasha")).get("current_dasha")).get("md"))
|
||
if language == "en":
|
||
return (f"Vimshottari MD {vim.get('lord') or '—'} ({vim.get('start') or '—'} to {vim.get('end') or '—'}); "
|
||
f"Narayana MD {nar.get('sign') or '—'} (lord {nar.get('lord') or '—'}, age {nar.get('start_age', '—')}–{nar.get('end_age', '—')}). "
|
||
"Both tracks are read together; when they point different ways both are stated.")
|
||
return (f"Vimshottari 当前大运 {_planet(vim.get('lord'), language) or '—'}({vim.get('start') or '—'} 至 {vim.get('end') or '—'});"
|
||
f"Narayana 当前大运 {_sign(nar.get('sign'), language) or '—'}(宫主 {_planet(nar.get('lord'), language) or '—'},"
|
||
f"{nar.get('start_age', '—')}–{nar.get('end_age', '—')} 岁)。双轨同读,方向不同时两面都写。")
|
||
|
||
|
||
def _audit_rows(packet: dict, language: str) -> list[tuple[str, str, str, str]]:
|
||
worksheets = _dict(packet.get("worksheets"))
|
||
functional = _dict(_dict(worksheets.get("strengths_and_scores")).get("functional_benefic_malefic"))
|
||
timing = _dict(worksheets.get("timing_and_predictive_systems"))
|
||
table = packet.get("technique_audit_table") if isinstance(packet.get("technique_audit_table"), list) else []
|
||
rows: list[tuple[str, str, str, str]] = []
|
||
seen = set()
|
||
for row in table:
|
||
if not isinstance(row, dict) or not row.get("technique"):
|
||
continue
|
||
name = str(row["technique"])
|
||
seen.add(name)
|
||
if name in HIDDEN_TECHNIQUES:
|
||
# 产品负责人 2026-11-08:这两行只在后台记录里保留,不渲染给用户。
|
||
continue
|
||
if name == "Functional Benefic/Malefic":
|
||
note = _functional_note(functional, language) if functional else _clean(row.get("note"), language)
|
||
elif name == "Vimshottari + Narayana Cross-check":
|
||
note = _dual_dasha_note(timing, language)
|
||
else:
|
||
note = _clean(row.get("note"), language)
|
||
rows.append((_clean(name, language), _status(row.get("status"), language), _source(row.get("source"), language), note))
|
||
if "Functional Benefic/Malefic" not in seen:
|
||
status = functional.get("status") or "blocked"
|
||
note = _functional_note(functional, language) if functional else (
|
||
"功能吉凶层缺失,本报告不能声称完成高严谨解读。" if language != "en"
|
||
else "Functional layer missing; this report cannot claim a high-rigor reading.")
|
||
rows.append(("Functional Benefic/Malefic", _status(status, language), _source("", language), note))
|
||
return rows
|
||
|
||
|
||
def _table(header: tuple[str, ...], rows: list[tuple[str, ...]]) -> list[str]:
|
||
lines = ["| " + " | ".join(header) + " |", "|" + "|".join("------" for _ in header) + "|"]
|
||
lines.extend("| " + " | ".join(cell or "—" for cell in row) + " |" for row in rows)
|
||
return lines
|
||
|
||
|
||
def _functional_table(packet: dict, language: str) -> list[str]:
|
||
functional = _dict(_dict(_dict(packet.get("worksheets")).get("strengths_and_scores")).get("functional_benefic_malefic"))
|
||
if not functional:
|
||
return []
|
||
benefic = set(functional.get("functional_benefics") or []) | set(functional.get("yogakarakas") or [])
|
||
malefic = set(functional.get("functional_malefics") or [])
|
||
owned = _dict(functional.get("owned_houses"))
|
||
en = language == "en"
|
||
rows = []
|
||
for planet in PLANETS:
|
||
natural = "benefic" if planet in NATURAL_BENEFICS else "malefic"
|
||
if planet in ("Rahu", "Ketu"):
|
||
role = "not assigned" if en else "不单独定"
|
||
elif planet in set(functional.get("yogakarakas") or []):
|
||
role = "Yogakaraka"
|
||
elif planet in benefic:
|
||
role = "functional benefic" if en else "功能吉"
|
||
elif planet in malefic:
|
||
role = "functional malefic" if en else "功能凶"
|
||
else:
|
||
role = "functionally neutral" if en else "中性"
|
||
functional_side = "benefic" if planet in benefic else "malefic" if planet in malefic else None
|
||
clash = functional_side is not None and functional_side != natural and planet not in ("Rahu", "Ketu")
|
||
houses = owned.get(planet)
|
||
rows.append((
|
||
_planet(planet, language),
|
||
(f"natural {natural}" if en else {"benefic": "自然吉", "malefic": "自然凶"}[natural]),
|
||
role,
|
||
", ".join(str(h) for h in houses) if isinstance(houses, list) and houses else "—",
|
||
("yes, confidence down one step" if en else "冲突,降一级置信度") if clash else ("no conflict" if en else "一致"),
|
||
))
|
||
heading = "### Functional vs natural benefic / malefic" if en else "### 功能吉凶与自然吉凶"
|
||
header = ("Planet", "Natural", "Functional", "Houses owned", "Conflict") if en else ("行星", "自然属性", "功能属性", "主管宫", "冲突")
|
||
lagna = functional.get("ascendant") or ""
|
||
intro = (f"Lagna {lagna}: functional roles come from house lordship for this ascendant. Rahu and Ketu take no separate role."
|
||
if en else f"上升 {_sign(lagna, language)}:功能吉凶按本上升的宫主身份判定;罗睺、计都不单独定功能吉凶。")
|
||
return [heading, "", intro, "", *_table(header, rows), ""]
|
||
|
||
|
||
PARAMETER_LABELS = {
|
||
"lahiri": ("Lahiri", "Lahiri"), "raman": ("Raman", "Raman"), "kp": ("KP", "KP"),
|
||
"true_chitra": ("True Chitra", "True Chitra"), "krishnamurti": ("KP", "KP"),
|
||
"mean": ("mean node", "平交点"), "true": ("true node", "真交点"),
|
||
"whole_sign": ("whole sign", "整宫制"), "placidus": ("Placidus", "Placidus"),
|
||
}
|
||
|
||
|
||
def _param(value: Any, language: str) -> str:
|
||
key = str(value or "").strip().lower()
|
||
label = PARAMETER_LABELS.get(key)
|
||
if label:
|
||
return label[0 if language == "en" else 1]
|
||
return _clean(value, language)
|
||
|
||
|
||
def _difference_rows(packet: dict, language: str) -> list[tuple[str, str, str, str]]:
|
||
en = language == "en"
|
||
profile = _dict(packet.get("calculation_profile"))
|
||
worksheets = _dict(packet.get("worksheets"))
|
||
advanced = _dict(worksheets.get("advanced_systems"))
|
||
timing = _dict(worksheets.get("timing_and_predictive_systems"))
|
||
kp_profile = _dict(_dict(advanced.get("kp")).get("kp_profile"))
|
||
rows = [
|
||
("Ayanamsa" if en else "岁差 Ayanamsa", _param(profile.get("ayanamsa"), language),
|
||
"other ayanamsas shift every sign boundary" if en else "换岁差会移动所有星座与分盘边界", _status("parameter_sensitive", language)),
|
||
("Node mode" if en else "交点 Node mode", _param(profile.get("node_mode"), language),
|
||
"true vs mean node" if en else "真交点 / 平交点", _status("parameter_sensitive", language)),
|
||
("House system" if en else "宫制", _param(profile.get("house_system"), language),
|
||
(f"KP uses {_param(kp_profile.get('house_system'), language)} cusps with {_param(kp_profile.get('ayanamsa'), language)} ayanamsa" if en
|
||
else f"KP 用 {_param(kp_profile.get('house_system'), language)} 宫头与 {_param(kp_profile.get('ayanamsa'), language)} 岁差,个别行星落宫可能不同"),
|
||
_status(kp_profile.get("status") or "parameter_sensitive", language)),
|
||
]
|
||
narayana = _dict(timing.get("narayana_dasha"))
|
||
current = _dict(_dict(narayana.get("current_dasha")).get("md"))
|
||
legacy = _dict(_dict(_dict(narayana.get("legacy")).get("current_dasha")).get("md"))
|
||
if current:
|
||
other = (f"legacy method: {legacy.get('sign')} (age {legacy.get('start_age')}–{legacy.get('end_age')})" if en
|
||
else f"旧算法:{_sign(legacy.get('sign'), language)}({legacy.get('start_age')}–{legacy.get('end_age')} 岁)") if legacy else "—"
|
||
rows.append((
|
||
"Narayana Dasha",
|
||
(f"{current.get('sign')} (age {current.get('start_age')}–{current.get('end_age')})" if en
|
||
else f"{_sign(current.get('sign'), language)}({current.get('start_age')}–{current.get('end_age')} 岁)"),
|
||
other,
|
||
_status("conflict" if legacy and legacy.get("sign") != current.get("sign") else narayana.get("status"), language),
|
||
))
|
||
divergences = _dict(timing.get("transit_multi_reference")).get("divergences")
|
||
for row in divergences if isinstance(divergences, list) else []:
|
||
if not isinstance(row, dict) or row.get("planet") not in ("Jupiter", "Saturn", "Rahu", "Ketu"):
|
||
continue
|
||
houses = _dict(row.get("houses"))
|
||
names = {"Lagna": ("Lagna", "上升"), "Chandra_Lagna": ("Moon", "月亮"), "Arudha_Lagna": ("Arudha Lagna", "AL"),
|
||
"Navamsa_Lagna": ("Navamsa Lagna", "D9 上升")}
|
||
text = ("; " if en else ";").join(
|
||
f"{names.get(key, (key, key))[0 if en else 1]} {value}" for key, value in houses.items()
|
||
)
|
||
rows.append((
|
||
(f"Transit {row['planet']} by reference point" if en else f"{_planet(row['planet'], language)}行运按参照点"),
|
||
text, "houses differ by reference point" if en else "不同参照点落宫不同,需合看", _status("parameter_sensitive", language),
|
||
))
|
||
return rows
|
||
|
||
|
||
FAMILY_NOTE_ZH = {
|
||
"parameter_sensitive": "本地已算出,外部同口径对拍未闭环;可作条件性参考。",
|
||
"blocked": "本地没有产出主运周期,本报告不用。",
|
||
"not_applicable": "按本盘的适用条件不适用。",
|
||
"partial": "只算出部分层级。",
|
||
"partial_verified": "部分核验。",
|
||
"verified": "已核验。",
|
||
}
|
||
|
||
|
||
def _dasha_family_rows(packet: dict, language: str) -> list[tuple[str, str, str]]:
|
||
timing = _dict(_dict(packet.get("worksheets")).get("timing_and_predictive_systems"))
|
||
families = _dict(_dict(timing.get("dasha_master_pack")).get("families"))
|
||
narayana = _dict(timing.get("narayana_dasha"))
|
||
rows = []
|
||
for name, family in families.items():
|
||
if not isinstance(family, dict):
|
||
continue
|
||
status = str(family.get("status") or "")
|
||
if name == "narayana" and _dict(narayana.get("current_dasha")).get("md"):
|
||
# The master pack does not run Narayana; the report's Narayana is the timing chapter's.
|
||
rows.append(("Narayana", _status(narayana.get("status") or "partial_verified", language),
|
||
"Read in the dasha chapter (default method, dated from the birth date plus age)." if language == "en"
|
||
else "在大运章节单独计算(默认算法,日期按出生日加年龄)。"))
|
||
continue
|
||
if language == "en":
|
||
reason = family.get("reason")
|
||
note = _clean(reason, language) if reason else "—"
|
||
else:
|
||
# The engine's reasons are English; the Chinese edition says what the status means.
|
||
note = FAMILY_NOTE_ZH.get(status, "—")
|
||
rows.append((_clean(name, language).title(), _status(status, language), note))
|
||
return rows
|
||
|
||
|
||
def _boundaries(packet: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
worksheets = _dict(packet.get("worksheets"))
|
||
timing = _dict(worksheets.get("timing_and_predictive_systems"))
|
||
double = _dict(timing.get("double_transit_pac"))
|
||
ledger = _dict(_dict(_dict(packet.get("full_report_pack")).get("sections")).get("d1_d60_ledger"))
|
||
lines = []
|
||
if en:
|
||
lines.append("- Yogas are rule matches (candidates). Cancellation conditions and real events are not checked, so no yoga is stated as confirmed.")
|
||
lines.append("- Timing is stated to the month at most; no statement names an exact day.")
|
||
if double:
|
||
lines.append("- Double Transit is a reference-date observation, not an event prediction or a calibrated timing result.")
|
||
if ledger:
|
||
lines.append("- Only the 20 formal divisions may be read, inside their own boundaries; the 40 research D-N rows are raw data.")
|
||
else:
|
||
lines.append("- Yoga 是按规则匹配出的候选;取消条件与现实事件没有核对,不写成已成立。")
|
||
lines.append("- 时间判断最多说到月份,不点具体哪一天。")
|
||
if double:
|
||
lines.append("- 双重行运(Double Transit)是参考日的观察,不是事件预测,也不是校准过的应期。")
|
||
if ledger:
|
||
lines.append("- 账本里只有 20 个传统分盘可以在各自边界内交叉阅读;其余 40 个研究型 D-N 只是原始数据,不单独下结论。")
|
||
return lines
|
||
|
||
|
||
def _position(value: Any, language: str) -> str:
|
||
if not isinstance(value, dict) or not value.get("sign"):
|
||
return "—"
|
||
degree = value.get("degree_in_sign", value.get("degree"))
|
||
if degree is None and isinstance(value.get("lon"), (int, float)):
|
||
degree = float(value["lon"]) % 30
|
||
sign = _sign(value.get("sign"), language)
|
||
return f"{sign} {float(degree):.2f}°" if isinstance(degree, (int, float)) and not isinstance(degree, bool) else sign
|
||
|
||
|
||
def render_d1_d60_ledger(packet: dict, language: str) -> list[str]:
|
||
en = language == "en"
|
||
ledger = _dict(_dict(_dict(packet.get("full_report_pack")).get("sections")).get("d1_d60_ledger"))
|
||
rows = _dict(ledger.get("d1_to_d60"))
|
||
if not rows:
|
||
return []
|
||
summary = _dict(ledger.get("summary"))
|
||
header = (("Division", *BODIES, "Tier", "Reading", "Stability") if en
|
||
else ("分盘", *(PLANET_ZH[body] for body in BODIES), "技法等级", "解释许可", "稳定性"))
|
||
body_rows = []
|
||
for division in (f"D{number}" for number in range(1, 61)):
|
||
row = _dict(rows.get(division))
|
||
raw = _dict(row.get("raw"))
|
||
formal = row.get("capability_tier") == "formal_traditional_division"
|
||
tier = ("formal division" if formal else "research D-N") if en else ("传统正式" if formal else "研究型 D-N")
|
||
allowed = ("bounded cross-reading" if en else "允许受限交叉阅读") if row.get("interpretation_allowed") else ("raw research only" if en else "仅原始研究")
|
||
stability = {"not_tested": ("not tested", "未测"), "stable": ("stable", "稳定"), "unstable": ("unstable", "不稳定")}.get(
|
||
str(row.get("stability") or "not_tested"), (_clean(row.get("stability"), language),) * 2)[0 if en else 1]
|
||
body_rows.append((division, *(_position(raw.get(body), language) for body in BODIES), tier, allowed, stability))
|
||
intro = (
|
||
f"All 60 divisions as raw positions. Formal traditional divisions: {summary.get('formal_traditional_division_count', '—')}; "
|
||
f"research generic D-N: {summary.get('research_generic_dn_division_count', '—')}. Named divisions are read only inside their own "
|
||
"evidence boundary; the research rows cannot produce a theme, an event or a chart approval."
|
||
if en else
|
||
f"保留全部 60 个分割数的原始位置。传统正式分盘 {summary.get('formal_traditional_division_count', '—')} 个;研究型通用 D-N "
|
||
f"{summary.get('research_generic_dn_division_count', '—')} 个。传统分盘只在各自的证据边界内进入专题交叉阅读;研究型 D-N 不独立生成主题结论、事件判断或选盘依据。"
|
||
)
|
||
return [f"## {LEDGER_HEADING['en' if en else 'zh']}", "", intro, "", *_table(header, body_rows), ""]
|
||
|
||
|
||
def render_audit_chapters(packet: dict, language: str) -> str:
|
||
language = "en" if language == "en" else "zh"
|
||
en = language == "en"
|
||
if not isinstance(packet, dict):
|
||
return ""
|
||
lines = [
|
||
f"## {AUDIT_HEADING[language]}",
|
||
"",
|
||
("This chapter lists what the report actually ran, what it could not run, where parameters or methods change the "
|
||
"answer, and the limits of each kind of evidence. Readers can skip it; it stays in every export."
|
||
if en else
|
||
"本章列出本报告实际调用了哪些技法、哪些没有运行、哪里换参数或换方法结果会变,以及各类证据能说到哪一步。普通阅读可以跳过,导出时始终保留。"),
|
||
"",
|
||
f"### {AUDIT_TABLE_HEADING[language]}",
|
||
"",
|
||
*_table(("Technique", "Status", "Source", "Audit note") if en else ("技法", "状态", "依据来源", "审计说明"),
|
||
_audit_rows(packet, language)),
|
||
"",
|
||
*_functional_table(packet, language),
|
||
]
|
||
differences = _difference_rows(packet, language)
|
||
if differences:
|
||
lines.extend([
|
||
"### Parameters and method differences (parameter-sensitive)" if en else "### 参数与方法差异(参数敏感)",
|
||
"",
|
||
*_table(("Item", "This report", "Comparison", "Status") if en else ("项目", "本报告取值", "对照", "状态"), differences),
|
||
"",
|
||
])
|
||
families = _dasha_family_rows(packet, language)
|
||
if families:
|
||
lines.extend([
|
||
"### Dasha systems: status" if en else "### 各大运体系状态",
|
||
"",
|
||
*_table(("System", "Status", "Note") if en else ("体系", "状态", "说明"), families),
|
||
"",
|
||
])
|
||
lines.extend(["### Evidence boundaries" if en else "### 证据边界", "", *_boundaries(packet, language), ""])
|
||
lines.extend(render_d1_d60_ledger(packet, language))
|
||
return "\n".join(lines).rstrip() + "\n"
|