新六章有 12 节读错公开包字段,整节只剩未判定,而且六章绕过了口径替换。按资料包改读路径,先拼六章再统一替换措辞,并把中文附录实际丢掉的组合句数出来。
Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Grok (agent loop)
Cursor
parent
365b1e647e
commit
92b4e220f2
@@ -0,0 +1,95 @@
|
||||
# PROGRESS:#91 A 段第 1 次打回(只修报告写法)
|
||||
|
||||
- 分支:`codex/full-data-parity-1-20261021`
|
||||
- 范围:修复单 J1–J4。B 段 T4–T10、#93 未做。
|
||||
- 状态:本地完成,未推送。
|
||||
|
||||
## 结论
|
||||
|
||||
0、1、9 号虚构盘的中英两版里,新六章除下面 4 节以外,每一节都至少有一行真实数据。这 4 节公开资料包没有对应模块,仍印「未判定」,第一行写明原因。
|
||||
|
||||
中文附录的年运表列名过滤在 200 张盘上没有丢掉任何列,所以没有补 `FIELD_ZH`。瑜伽「作用」的粘连过滤也没有删条目。瑜伽「组合」会把英文句子整格印成「未判定」:200 张盘全部中招,去重后 1030 条,源头不改。
|
||||
|
||||
## J1 十二节
|
||||
|
||||
读的是 `build_professional_report_reference_packet` 放进公开工作表的位置。0、1、9 号盘中英两版都由测试确认至少一行不是「未判定 / Not determined」。
|
||||
|
||||
| 节 | 公开包位置 |
|
||||
| --- | --- |
|
||||
| Vimsopaka 分盘贡献 | `strengths_and_scores.vimsopaka` 下直接是行星键(分数、分档) |
|
||||
| Karakamsha | `advanced_systems.jaimini.karakamsha` 的星座、度数、宫主 |
|
||||
| Darakaraka | `advanced_systems.jaimini.darakaraka` 的 `dk_planet`、`dk_sign`、`dk_house`、`dk_degree`、`d9_sign`、`d9_dignity` |
|
||||
| D9 尊贵 | `divisional_and_special_charts.varga_full.D9_Navamsa`,行星是直接键,尊贵在 `_dignity` |
|
||||
| 分盘映射 | 同上各分盘的 `Ascendant.sign`。盘名印成 `D9 Navamsa` 这种,不印带下划线的键。D81 / D108 / D144 不进这张表 |
|
||||
| D30 状态 | `varga_full.D30_Trimsamsa` 与 `_dignity` |
|
||||
| D30 输入 | `strengths_and_scores.shadbala.planets.*.sthana_bala.sapta_d30` |
|
||||
| 行星状态输入 | `advanced_systems.avasthas.<planet>.avasthas` 的 Jagrat / Deeptadi 状态,外加度数 |
|
||||
| 定位星终点与循环 | `advanced_systems.final_dispositors`。自循环印主星,多星循环印「循环 / Cycle」 |
|
||||
| Sudarshana 参考点 | `divisional_and_special_charts.sudarshana.raw.reference_points` |
|
||||
| Bhava Chalit 宫心 | `divisional_and_special_charts.bhava_chalit.raw.boundaries.houses` |
|
||||
| 本命特殊点公式依据 | `advanced_systems.sahams` 里带星座或黄经的点,印名称、星座、度数、黄经。不印公式轨迹 |
|
||||
| 注明日期的行运 | `timing_and_predictive_systems.transit_multi_reference` 的 `transit_analysis` 与 `target_date`,星宿来自 `advanced_systems.nakshatra_dasha.transit_overlay` |
|
||||
|
||||
## 四节白名单
|
||||
|
||||
公开报告资料包未带这些模块(只在被拿掉的原始阅读结果里)。每节第一行是「公开报告资料包未带此模块」/ `This module is not in the public report packet`,表内仍是「未判定」。没有为它们改引擎。
|
||||
|
||||
| 节 | 原因 |
|
||||
| --- | --- |
|
||||
| Navamsha 小运时长 | `navamsha_dasha` 没有复制进公开工作表 |
|
||||
| 五鸟活动 | `pancha_pakshi` 没有复制进公开工作表 |
|
||||
| Rashi Tulya Navamsa | `rashi_tulya_navamsa` 没有复制进公开工作表 |
|
||||
| 当前多系统收敛 | `dasa_convergence` 的逐领域行没有复制进公开工作表。不再用 `convergence_top_domains` 充数 |
|
||||
|
||||
## J2 / J3
|
||||
|
||||
`tests/test_full_data_parity_chapters.py` 的 `test_every_section_has_a_real_cell_except_the_four_public_gaps`:0、1、9 号盘中英两版,白名单外每一节至少有一个数据单元格不是「未判定 / Not determined」。表头和分隔行不算。白名单只有上表 4 节,每项带了原因。
|
||||
|
||||
`test_parity_chapters_go_through_local_conventions`:六章先拼进正文,再走 `_apply_local_report_conventions`。中文源字段「必然」渲染成该函数的结果「可能」;英文字段 `divorce` 渲染成 `relationship strain`。
|
||||
|
||||
## J4 丢弃统计
|
||||
|
||||
200 张虚构盘,PyJHora 已屏蔽。汉字为 0。计数在中文渲染里,同一条若被阅读表、资料附录和解释包各走一次,调用次数会高于条目数。条目数是每张盘内按规则和原文去重后再相加。
|
||||
|
||||
| 处 | 丢掉了什么 | 条目 | 调用 | 受影响盘 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 年运表列名 | 无。过滤执行了 800 次(每盘 4 张表),没有列被 `FIELD_ZH` 拿掉 | 0 | 800 次过滤 | 0 |
|
||||
| 瑜伽「作用」 | 无。粘连过滤没有删掉任何一条 | 0 | 0 | 0 |
|
||||
| 瑜伽「组合」 | 非空的组合句整格变成「未判定」 | 1030 | 2496 | 200 |
|
||||
|
||||
年运表优先列本来就有中文名,所以没有新列要补进 `FIELD_ZH`。`reading` 这类额外字段到不了这道过滤:优先列已经有值,回退列名单不会启用。
|
||||
|
||||
「作用」里的英文整句没有被删。粘连规则只删「汉字紧挨 in/and/or/from/planet/lord」、英文星座名,以及 `, :` / `in )`。BVR 规则的 `effects` 要么是纯中文,要么是纯英文,纯英文留在格子里。本单不改 `references/yoga_rules.json`。
|
||||
|
||||
「组合」变成「未判定」的源头:
|
||||
|
||||
- `scripts/yoga_engine.py` 的 `_eval_semantic_condition`:没有中文模板时,用规则里的英文 `note` 当组合句。规则在 `references/yoga_rules.json`。按渲染调用次数:`bvr_krisanga_yoga` 501,`bvr_budha_aaditya_yoga` 315,`bvr_paasa_yoga` 282,`bvr_kedaara_yoga` 162,`bvr_vanchana_chora_bheethi_yoga` 144,`bvr_dehasthoulya_yoga` 125,`bvr_daama_yoga` 126,`bvr_kalpadruma_yoga` 108,`bvr_bahu_sthree_yoga` 102,`bvr_bhaarathi_yoga` 69,`bvr_grihanasa_yoga` 66,`bvr_asubha_yoga` 45,`bvr_subha_yoga` 45,`bvr_bandhu_pujya_yoga` 54,`bvr_guhyaroga_yoga` 27(句中有英文星座名),`bvr_vallaki_yoga` 18,`bvr_soola_yoga` 12,`bvr_rajalakshana_yoga` 9,`bvr_kalaanidhi_yoga` 3。
|
||||
- 没有 `rule_id` 的 283 次来自 `scripts/yogas_doshas.py`:`calc_raj_yogas` 与 `calc_dhana_yogas` 写出 `H*_Lord(...) in H*` 这种英文组合句。句子里的行星随盘变化,这里不列具体星。
|
||||
|
||||
这些句子是中文报告在用的英文字段。本单只计数,不改规则文件,也不改这两个函数。
|
||||
|
||||
## 阅读页上的英文星座名
|
||||
|
||||
`frontend/src/lib/personal-report-generation.ts` 的 `assembleReportDocumentV2`。它在组装 v2 报告文档时,把 D1 事实里的英文 `sign` 写进「本命基础」的 `narrative`,以及 `keyFactors` 里「第 N 宫落 {sign}」;宫内对象还是英文行星标识,两处句子加这一列,改一处星座映射盖不住。
|
||||
|
||||
阅读页 `frontend/src/components/personal-report/personal-report-page.tsx` 渲染的是 Markdown(`PersonalReportMarkdownView`)、盘图和事实表,不渲染这段 `natalFoundation`。`frontend/src/lib/report-public-projection.ts` 的 `foundationSection` 只是把它放到接口文档上。导出图盘列表的 `houseLine` 已经用中文星座名。本单不改前端,留给 Claude。
|
||||
|
||||
## 测试
|
||||
|
||||
解释器:`G:\Ferti\Jyotisha\.worktrees\agent-loop\.venv\Scripts\python.exe`。`PYTHONUTF8=1`。虚构盘助手已屏蔽 PyJHora。未跑快速门、全量 pytest、前端全量 `npm test`、`next build`。未改前端,未跑 `tsc` / `lint`。
|
||||
|
||||
| 命令 | 退出码 |
|
||||
| --- | --- |
|
||||
| `pytest tests/test_full_data_parity_chapters.py -q --tb=line` | 0(7 项,约 61 秒) |
|
||||
| `pytest` 附录、英文冒烟、`test_report_english_edition`、`test_report_full_data_fix`、`test_full_data_domain_reader`、`test_python_hygiene_budget`、`test_api_server_growth_contract`、`test_runtime_jhora_import_scan`、`test_sidereal_nutation_frame`、`test_rashi_dashas_native`、`test_bhava_bala_formal`、`test_year_lord_blocked_no_fallback`、`test_year_lord_basis_label`、`test_repo_privacy_markers` | 0(172 项,约 925 秒) |
|
||||
| `pytest tests/test_report_english_sweep.py -q --tb=line` | 0(200 张,约 1293 秒;汉字 0) |
|
||||
|
||||
隐私标记在写完本文之后再跑一次。
|
||||
|
||||
## 偏离
|
||||
|
||||
- 没有推送,没有设上游,没有改标签,没有发 Gitea 评论。
|
||||
- 没有改 `scripts/jyotish_api_server.py`、`scripts/jaimini.py`、冻结打分文件、`references/yoga_rules.json`。没有提高 `ROOT_PY_BUDGET`。
|
||||
- 没有新的 BUG 编号。BUG-1416、BUG-1417 保持不变。
|
||||
- 年运列和瑜伽「作用」的实测丢弃是 0,所以没有补中文列名,也没有改瑜伽英文源。
|
||||
- 组合句的 1030 条是「非空原文被印成未判定」的盘内去重数,比「句中正好有 in/and/or/from/planet/lord」的范围稍宽,多出来的是纯英文说明句。调用次数 2496 是同一条被多处渲染重复计入。
|
||||
@@ -523,4 +523,4 @@
|
||||
| `TASK-full-data-parity-3-20261022.md` | — | #93 完整数据版补齐(三):「独立年度方法参考」正式环境调 PyJHora(子进程隔离;装上后其余输出逐字节不变,7 个换路径开关钉死);输入默认用本地时间与正确年份(D6 待产品确认) | 待领取(等 #91 合入) | `codex/full-data-parity-3-20261022` |
|
||||
| `TASK-synastry-staging-fix-fix-20261022.md` | `PROGRESS-synastry-staging-fix-fix-20261022.md` | **#85 第 1 次打回**(T7~T11 主体通过):新地址没登记进路由清单致快速门红;CHANGELOG 与 T11 矛盾;侧栏点亮测试是空断言 | 已合入 staging | `codex/synastry-staging-fix-20261021` |
|
||||
| `TASK-report-english-han-leak-fix-20261022.md` | `PROGRESS-report-english-han-leak-fix-20261022.md` | **#89 第 1 次打回**:改了冻结的校正打分文件 `jaimini.py`;测试的 PyJHora 屏蔽没传到子进程(测的不是生产那份报告,也是慢的原因);缺英文时留空藏住漏洞 | 已合入 staging | `codex/report-english-han-leak-20261021` |
|
||||
| `TASK-full-data-parity-1-fix-20261022.md` | `PROGRESS-full-data-parity-1-20261021.md` | **#91 A 段第 1 次打回**:新六章 69 节里 16 节在三张虚构盘上整节「未判定」,其中 12 节数据在资料包里、是读错了字段;测试抓不到整节未判定;新六章绕过了报告口径替换;中文附录悄悄丢列和瑜伽条目要量出来 | 待执行 | `codex/full-data-parity-1-20261021` |
|
||||
| `TASK-full-data-parity-1-fix-20261022.md` | `PROGRESS-full-data-parity-1-fix-20261022.md` | **#91 A 段第 1 次打回**:新六章 69 节里 16 节在三张虚构盘上整节「未判定」,其中 12 节数据在资料包里、是读错了字段;测试抓不到整节未判定;新六章绕过了报告口径替换;中文附录悄悄丢列和瑜伽条目要量出来 | 本地完成,待验收 | `codex/full-data-parity-1-20261021` |
|
||||
|
||||
@@ -2107,9 +2107,23 @@ def _render_pl9_ai_density_raw_data_markdown_en(packet: dict, language: str = "e
|
||||
][:6]
|
||||
if language == "zh":
|
||||
try:
|
||||
from scripts.report_render.appendix_language import FIELD_ZH
|
||||
from scripts.report_render.appendix_language import (
|
||||
ANNUAL_COLUMN_NAMES_BEFORE_FIX,
|
||||
FIELD_ZH,
|
||||
note_annual_table,
|
||||
note_dropped_annual_column,
|
||||
)
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
from report_render.appendix_language import FIELD_ZH
|
||||
from report_render.appendix_language import (
|
||||
ANNUAL_COLUMN_NAMES_BEFORE_FIX,
|
||||
FIELD_ZH,
|
||||
note_annual_table,
|
||||
note_dropped_annual_column,
|
||||
)
|
||||
note_annual_table()
|
||||
for field in keys:
|
||||
if field not in ANNUAL_COLUMN_NAMES_BEFORE_FIX:
|
||||
note_dropped_annual_column(field)
|
||||
keys = [field for field in keys if field in FIELD_ZH]
|
||||
if not keys:
|
||||
continue
|
||||
@@ -2365,6 +2379,12 @@ def _append_parity_chapters(markdown: str, packet: dict, language: str) -> str:
|
||||
|
||||
def render_full_data_markdown(packet: dict) -> str:
|
||||
"""Upstream `pl9_ai_density` branch at 23be1807, on this site's parity body."""
|
||||
if _pl9_report_language(packet if isinstance(packet, dict) else {}) != "en":
|
||||
try:
|
||||
from scripts.report_render.appendix_language import reset_appendix_drop_log
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
from report_render.appendix_language import reset_appendix_drop_log
|
||||
reset_appendix_drop_log()
|
||||
attach_full_data_interpretation, opening_focus_block = _interpretation_tools()
|
||||
if isinstance(packet, dict):
|
||||
attach_full_data_interpretation(packet)
|
||||
@@ -2396,8 +2416,8 @@ def render_full_data_markdown(packet: dict) -> str:
|
||||
english_markdown,
|
||||
_pl9_english_report_title(packet),
|
||||
)
|
||||
rendered = _apply_local_report_conventions(rendered, language="en")
|
||||
return _append_parity_chapters(rendered, packet, "en")
|
||||
rendered = _append_parity_chapters(rendered, packet, "en")
|
||||
return _apply_local_report_conventions(rendered, language="en")
|
||||
zh_structured_appendix = re.sub(
|
||||
r"^#\s+.*?(?:\n{2,}|\n)",
|
||||
"",
|
||||
@@ -2418,8 +2438,8 @@ def render_full_data_markdown(packet: dict) -> str:
|
||||
+ "\n\n</details>\n"
|
||||
)
|
||||
rendered = _sanitize_pl9_ai_density_markdown(zh_markdown, _pl9_public_report_title(packet))
|
||||
rendered = _apply_local_report_conventions(rendered, language="zh")
|
||||
return _append_parity_chapters(rendered, packet, "zh")
|
||||
rendered = _append_parity_chapters(rendered, packet, "zh")
|
||||
return _apply_local_report_conventions(rendered, language="zh")
|
||||
|
||||
|
||||
def read_full_data_domain(packet: Mapping, domain: str) -> dict:
|
||||
|
||||
@@ -368,6 +368,63 @@ FIELD_ZH = {
|
||||
"months": "月数",
|
||||
}
|
||||
|
||||
# Names that already had a Chinese header before the #91 fix.
|
||||
# The annual-table filter used to drop every other column. Counts use this set
|
||||
# so a later Chinese name still shows up as a column the old filter removed.
|
||||
ANNUAL_COLUMN_NAMES_BEFORE_FIX = frozenset(FIELD_ZH)
|
||||
|
||||
_APPENDIX_DROPS: dict[str, dict[str, int]] = {
|
||||
"annual_columns": {},
|
||||
"annual_tables": {},
|
||||
"effects": {},
|
||||
"combinations_undetermined": {},
|
||||
"combinations_replaced": {},
|
||||
}
|
||||
|
||||
|
||||
def reset_appendix_drop_log() -> None:
|
||||
"""Clear the per-render drop counts. One Chinese render, then snapshot."""
|
||||
for bucket in _APPENDIX_DROPS.values():
|
||||
bucket.clear()
|
||||
|
||||
|
||||
def snapshot_appendix_drop_log() -> dict[str, dict[str, int]]:
|
||||
return {name: dict(bucket) for name, bucket in _APPENDIX_DROPS.items()}
|
||||
|
||||
|
||||
def note_dropped_annual_column(field: str) -> None:
|
||||
key = str(field)
|
||||
bucket = _APPENDIX_DROPS["annual_columns"]
|
||||
bucket[key] = bucket.get(key, 0) + 1
|
||||
|
||||
|
||||
def note_annual_table() -> None:
|
||||
bucket = _APPENDIX_DROPS["annual_tables"]
|
||||
bucket["seen"] = bucket.get("seen", 0) + 1
|
||||
|
||||
|
||||
def _drop_identity(item: dict, text: str) -> str:
|
||||
parts = [
|
||||
str(item.get("rule_id") or ""),
|
||||
str(item.get("source") or ""),
|
||||
str(item.get("bvr_id") or ""),
|
||||
str(item.get("name") or ""),
|
||||
" ".join(str(text).split()),
|
||||
]
|
||||
return " | ".join(parts)[:240]
|
||||
|
||||
|
||||
def _note_effect(item: dict, text: str) -> None:
|
||||
key = _drop_identity(item, text)
|
||||
bucket = _APPENDIX_DROPS["effects"]
|
||||
bucket[key] = bucket.get(key, 0) + 1
|
||||
|
||||
|
||||
def _note_combination(item: dict, text: str, kind: str) -> None:
|
||||
key = _drop_identity(item, text)
|
||||
bucket = _APPENDIX_DROPS[kind]
|
||||
bucket[key] = bucket.get(key, 0) + 1
|
||||
|
||||
|
||||
def column_header(field: str, language: str) -> str:
|
||||
if language != "zh":
|
||||
@@ -591,12 +648,19 @@ def chinese_yoga_label(item: dict) -> str:
|
||||
def chinese_combination(item: dict) -> str:
|
||||
"""Use the Chinese combination sentence. An English sentence is not translated word by word."""
|
||||
combination = str(item.get("combination") or "").strip()
|
||||
if combination and re.search(r"[\u4e00-\u9fff]", combination) and not _HAN_FUNCTION_RE.search(combination) and not SIGN_EN_RE.search(combination):
|
||||
han = bool(re.search(r"[\u4e00-\u9fff]", combination))
|
||||
function = bool(_HAN_FUNCTION_RE.search(combination)) if combination else False
|
||||
sign = bool(SIGN_EN_RE.search(combination)) if combination else False
|
||||
if combination and han and not function and not sign:
|
||||
return combination
|
||||
planets = item.get("planets") if isinstance(item.get("planets"), list) else []
|
||||
names = [planet_name(planet, "zh") for planet in planets if planet]
|
||||
if names:
|
||||
if combination:
|
||||
_note_combination(item, combination, "combinations_replaced")
|
||||
return "与".join(names)
|
||||
if combination:
|
||||
_note_combination(item, combination, "combinations_undetermined")
|
||||
return "未判定"
|
||||
|
||||
|
||||
@@ -610,7 +674,15 @@ def yoga_cells(item: dict, language: str) -> list[Any]:
|
||||
category_text = "未判定"
|
||||
strength = item.get("strength") or "-"
|
||||
effects = item.get("effects") if isinstance(item.get("effects"), list) else []
|
||||
kept = [str(effect) for effect in effects if str(effect).strip() and not _has_prose_glue(str(effect))]
|
||||
kept = []
|
||||
for effect in effects:
|
||||
text = str(effect).strip()
|
||||
if not text:
|
||||
continue
|
||||
if _has_prose_glue(text):
|
||||
_note_effect(item, text)
|
||||
continue
|
||||
kept.append(text)
|
||||
effects_text = ";".join(kept) or "-"
|
||||
return [chinese_yoga_label(item), category_text, strength, chinese_combination(item), effects_text]
|
||||
|
||||
|
||||
@@ -470,6 +470,84 @@ def _rows_from(items: list[Any], language: str, columns: list[tuple[str, str]])
|
||||
return _md_table(headers, rows)
|
||||
|
||||
|
||||
_PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
|
||||
_MISSING_MODULE = {
|
||||
"zh": "公开报告资料包未带此模块",
|
||||
"en": "This module is not in the public report packet",
|
||||
}
|
||||
|
||||
|
||||
def _missing_module(language: str, columns: list[str]) -> str:
|
||||
"""The public worksheet does not carry this module. Keep the undetermined row."""
|
||||
return _MISSING_MODULE[language] + "\n\n" + _blank(language, columns)
|
||||
|
||||
|
||||
def _is_planet(value: Any) -> bool:
|
||||
return isinstance(value, str) and value.strip() in _PLANETS
|
||||
|
||||
|
||||
def _dignity_token(value: Any, language: str) -> Any:
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
key = " ".join(value.strip().split()).lower()
|
||||
if key == "neutral":
|
||||
return "中性" if language == "zh" else "Neutral"
|
||||
return {
|
||||
"exalted": "exalted",
|
||||
"debilitated": "debilitated",
|
||||
"own sign": "own",
|
||||
"own": "own",
|
||||
"moolatrikona": "mooltrikona",
|
||||
}.get(key, value)
|
||||
|
||||
|
||||
def _named_planets(chart: Any) -> list[tuple[str, dict]]:
|
||||
if not isinstance(chart, dict):
|
||||
return []
|
||||
nested = chart.get("planets")
|
||||
source = nested if isinstance(nested, dict) else chart
|
||||
rows = []
|
||||
for name in _PLANETS:
|
||||
item = source.get(name)
|
||||
if isinstance(item, dict):
|
||||
rows.append((name, item))
|
||||
return rows
|
||||
|
||||
|
||||
def _varga_chart(packet: dict, *keys: str) -> dict:
|
||||
varga = _first(packet, ("divisional_and_special_charts", "varga_full")) or {}
|
||||
if not isinstance(varga, dict):
|
||||
return {}
|
||||
for key in keys:
|
||||
chart = varga.get(key)
|
||||
if isinstance(chart, dict):
|
||||
return chart
|
||||
return {}
|
||||
|
||||
|
||||
def _varga_label(name: str, chart: dict) -> str:
|
||||
meta = chart.get("_meta") if isinstance(chart.get("_meta"), dict) else {}
|
||||
div = meta.get("div")
|
||||
title = str(meta.get("name") or "")
|
||||
if div and title and "_" not in title and not re.search(r"[\u4e00-\u9fff]", title):
|
||||
return f"D{div} {title}".strip()
|
||||
return " ".join(str(name).replace("_", " ").split())
|
||||
|
||||
|
||||
def _cycle_label(value: str, language: str) -> str | None:
|
||||
body = value.split(":", 1)[1]
|
||||
parts = [part.strip(" .") for part in body.replace("->", "|").replace("→", "|").split("|")]
|
||||
names = []
|
||||
for part in parts:
|
||||
if not _is_planet(part):
|
||||
return None
|
||||
names.append(planet_name(part, language))
|
||||
if len(names) < 2:
|
||||
return None
|
||||
prefix = "循环" if language == "zh" else "Cycle"
|
||||
return prefix + " " + " → ".join(names)
|
||||
|
||||
|
||||
def _months(start: Any, end: Any) -> str | None:
|
||||
if not isinstance(start, str) or not isinstance(end, str):
|
||||
return None
|
||||
@@ -560,25 +638,8 @@ def _tribhagi_periods(packet: dict, language: str) -> str:
|
||||
|
||||
|
||||
def _navamsha(packet: dict, language: str) -> str:
|
||||
module = _first(packet, ("timing_and_predictive_systems", "navamsha_dasha"))
|
||||
periods = []
|
||||
if isinstance(module, dict):
|
||||
majors = module.get("periods") or module.get("native_result", {}).get("periods") if isinstance(module.get("native_result"), dict) else module.get("periods")
|
||||
if isinstance(module.get("native_result"), dict):
|
||||
majors = module["native_result"].get("periods") or majors
|
||||
for major in majors or []:
|
||||
if not isinstance(major, dict):
|
||||
continue
|
||||
for child in major.get("antardasha") or []:
|
||||
if not isinstance(child, dict):
|
||||
continue
|
||||
copied = dict(child)
|
||||
copied["months"] = child.get("duration_months") or _months(child.get("start"), child.get("end"))
|
||||
periods.append(copied)
|
||||
if not periods:
|
||||
note = "公开报告包没有带上 Navamsha 小运行。" if language == "zh" else "The public packet does not carry the Navamsha sub-period rows."
|
||||
return note + "\n\n" + _blank(language, ["sign", "start", "end", "months"])
|
||||
return _rows_from(periods, language, [("sign", "sign"), ("start", "text"), ("end", "text"), ("months", "text")])
|
||||
del packet
|
||||
return _missing_module(language, ["sign", "start", "end", "months"])
|
||||
|
||||
|
||||
def _nakshatra_context(packet: dict, language: str) -> str:
|
||||
@@ -675,31 +736,8 @@ def _sandhi(packet: dict, language: str) -> str:
|
||||
|
||||
|
||||
def _convergence(packet: dict, language: str) -> str:
|
||||
timing = _first(packet, ("ai_and_audit", "ai_prompt_pack", "evidence_snapshot", "timing")) or {}
|
||||
pairs = timing.get("convergence_top_domains") if isinstance(timing, dict) else None
|
||||
module = _first(packet, ("timing_and_predictive_systems", "dasa_convergence"))
|
||||
activations = module.get("domain_activations") if isinstance(module, dict) else None
|
||||
rows = []
|
||||
if isinstance(activations, dict) and activations:
|
||||
for domain, info in activations.items():
|
||||
if not isinstance(info, dict):
|
||||
continue
|
||||
rows.append({
|
||||
"domain": domain,
|
||||
"house": info.get("house"),
|
||||
"level": info.get("convergence_level"),
|
||||
"counted": info.get("system_count"),
|
||||
})
|
||||
elif isinstance(pairs, list):
|
||||
for item in pairs:
|
||||
if isinstance(item, (list, tuple)) and len(item) >= 2:
|
||||
rows.append({"domain": item[0], "level": item[1]})
|
||||
elif isinstance(item, dict):
|
||||
rows.append({"domain": item.get("domain") or item.get("name"), "level": item.get("level") or item.get("convergence_level")})
|
||||
if not rows:
|
||||
note = "公开报告包没有带上逐领域激活行。" if language == "zh" else "The public packet does not carry the per-domain activation rows."
|
||||
return note + "\n\n" + _blank(language, ["domain", "level"])
|
||||
return _rows_from(rows, language, [("domain", "text"), ("house", "text"), ("level", "text"), ("counted", "text")])
|
||||
del packet
|
||||
return _missing_module(language, ["domain", "level"])
|
||||
|
||||
|
||||
def _chara(packet: dict, language: str) -> str:
|
||||
@@ -890,12 +928,30 @@ def _bhava(packet: dict, language: str) -> str:
|
||||
|
||||
def _vimsopaka(packet: dict, language: str) -> str:
|
||||
module = _first(packet, ("strengths_and_scores", "vimsopaka")) or {}
|
||||
nested = module.get("planets") if isinstance(module, dict) else None
|
||||
planets = nested if isinstance(nested, dict) else module
|
||||
bands = {
|
||||
"High": ("High", "高"),
|
||||
"Moderate": ("Moderate", "中"),
|
||||
"Limited": ("Limited", "有限"),
|
||||
"Low": ("Low", "低"),
|
||||
}
|
||||
rows = []
|
||||
planets = module.get("planets") if isinstance(module, dict) else module
|
||||
if isinstance(planets, dict):
|
||||
for planet, item in planets.items():
|
||||
if isinstance(item, dict) and ("total_score" in item or "category" in item):
|
||||
rows.append({"planet": planet, "score": item.get("total_score"), "category": item.get("category")})
|
||||
if not isinstance(item, dict) or ("total_score" not in item and "category" not in item):
|
||||
continue
|
||||
if planet not in _PLANETS and item.get("planet") not in _PLANETS:
|
||||
continue
|
||||
category = item.get("category")
|
||||
pair = bands.get(str(category))
|
||||
if pair:
|
||||
category = pair[1] if language == "zh" else pair[0]
|
||||
rows.append({
|
||||
"planet": planet if planet in _PLANETS else item.get("planet"),
|
||||
"score": item.get("total_score"),
|
||||
"category": category,
|
||||
})
|
||||
elif isinstance(planets, list):
|
||||
rows = [item for item in planets if isinstance(item, dict)]
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("score", "text"), ("category", "text")])
|
||||
@@ -958,33 +1014,61 @@ def _prastara(packet: dict, language: str) -> str:
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("total", "text")])
|
||||
|
||||
|
||||
def _avastha_state(item: dict) -> Any:
|
||||
states = item.get("avasthas") if isinstance(item.get("avasthas"), dict) else {}
|
||||
for key in ("Jagrat", "Deeptadi"):
|
||||
entry = states.get(key)
|
||||
if isinstance(entry, dict) and entry.get("state"):
|
||||
return entry.get("state")
|
||||
return item.get("jagradadi") or item.get("state") or item.get("avastha")
|
||||
|
||||
|
||||
def _avastha(packet: dict, language: str) -> str:
|
||||
module = _first(packet, ("advanced_systems", "avasthas")) or {}
|
||||
nested = module.get("planets") if isinstance(module, dict) else None
|
||||
planets = nested if isinstance(nested, dict) else module if isinstance(module, dict) else {}
|
||||
rows = []
|
||||
planets = module.get("planets") if isinstance(module, dict) else module
|
||||
if isinstance(planets, dict):
|
||||
for planet, item in planets.items():
|
||||
if isinstance(item, dict):
|
||||
rows.append({"planet": planet, "status": item.get("jagradadi") or item.get("state") or item.get("avastha"), "degree": item.get("degree") or item.get("degree_in_sign")})
|
||||
elif isinstance(planets, list):
|
||||
rows = [item for item in planets if isinstance(item, dict)]
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("status", "text"), ("degree", "text")])
|
||||
for planet, item in planets.items():
|
||||
if planet not in _PLANETS or not isinstance(item, dict):
|
||||
continue
|
||||
degree = item.get("degree")
|
||||
if degree is None:
|
||||
degree = item.get("degree_in_sign")
|
||||
rows.append({
|
||||
"planet": planet,
|
||||
"sign": item.get("sign"),
|
||||
"status": _avastha_state(item),
|
||||
"degree": degree,
|
||||
})
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("status", "text"), ("degree", "text")])
|
||||
|
||||
|
||||
def _d30(packet: dict, language: str, source: bool) -> str:
|
||||
module = _first(packet, ("advanced_systems", "trimshamsa_d30")) or _first(packet, ("divisional_and_special_charts", "varga_full", "D30"))
|
||||
rows = []
|
||||
planets = module.get("planets") if isinstance(module, dict) else None
|
||||
if isinstance(planets, dict):
|
||||
for planet, item in planets.items():
|
||||
if not isinstance(item, dict):
|
||||
if source:
|
||||
rows = []
|
||||
for planet, item in _shadbala_planets(packet).items():
|
||||
if planet not in _PLANETS or not isinstance(item, dict):
|
||||
continue
|
||||
if source:
|
||||
rows.append({"planet": planet, "degree": item.get("degree_in_sign") or item.get("degree"), "sign": item.get("sign")})
|
||||
else:
|
||||
rows.append({"planet": planet, "sign": item.get("sign"), "status": item.get("dignity") or item.get("status")})
|
||||
columns = [("planet", "planet"), ("degree", "text"), ("sign", "sign")] if source else [("planet", "planet"), ("sign", "sign"), ("status", "text")]
|
||||
return _rows_from(rows, language, columns)
|
||||
sthana = item.get("sthana_bala") if isinstance(item.get("sthana_bala"), dict) else {}
|
||||
score = sthana.get("sapta_d30")
|
||||
if isinstance(score, bool) or not isinstance(score, (int, float)):
|
||||
continue
|
||||
rows.append({"planet": planet, "score": score})
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("score", "text")])
|
||||
chart = _varga_chart(packet, "D30_Trimsamsa", "D30", "Trimsamsa")
|
||||
if not chart:
|
||||
legacy = _first(packet, ("advanced_systems", "trimshamsa_d30"))
|
||||
chart = legacy if isinstance(legacy, dict) else {}
|
||||
dignity = chart.get("_dignity") if isinstance(chart.get("_dignity"), dict) else {}
|
||||
rows = []
|
||||
for planet, item in _named_planets(chart):
|
||||
status = dignity.get(planet) or item.get("dignity") or item.get("status")
|
||||
rows.append({
|
||||
"planet": planet,
|
||||
"sign": item.get("sign"),
|
||||
"status": _dignity_token(status, language),
|
||||
})
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("status", "text")])
|
||||
|
||||
|
||||
def _pushkara(packet: dict, language: str) -> str:
|
||||
@@ -1112,8 +1196,26 @@ def _chain_rows(module: Any, terminal: bool) -> list[dict]:
|
||||
|
||||
|
||||
def _dispositor(packet: dict, language: str, terminal: bool) -> str:
|
||||
module = _first(packet, ("advanced_systems", "final_dispositors" if terminal else "dispositor_chains"))
|
||||
return _rows_from(_chain_rows(module, terminal), language, [("planet", "planet"), ("sign", "sign"), ("lord", "planet")])
|
||||
if not terminal:
|
||||
module = _first(packet, ("advanced_systems", "dispositor_chains"))
|
||||
return _rows_from(_chain_rows(module, False), language, [("planet", "planet"), ("sign", "sign"), ("lord", "planet")])
|
||||
module = _first(packet, ("advanced_systems", "final_dispositors")) or {}
|
||||
rows = []
|
||||
if isinstance(module, dict):
|
||||
for planet, value in module.items():
|
||||
if planet not in _PLANETS:
|
||||
continue
|
||||
if _is_planet(value):
|
||||
rows.append({"planet": planet, "lord": value})
|
||||
elif isinstance(value, str) and value.startswith("Cycle:"):
|
||||
label = _cycle_label(value, language)
|
||||
if label:
|
||||
rows.append({"planet": planet, "value": label})
|
||||
elif isinstance(value, (dict, list)):
|
||||
rows.extend(_chain_rows({planet: value}, True))
|
||||
if not rows:
|
||||
rows = _chain_rows(module, True)
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("lord", "planet"), ("value", "text")])
|
||||
|
||||
|
||||
def _aspects(packet: dict, language: str) -> str:
|
||||
@@ -1290,14 +1392,18 @@ def _saham_basis(packet: dict, language: str) -> str:
|
||||
for name, item in module.items():
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if "formula_status" not in item and "formula_variant" not in item and "formula_operands" not in item:
|
||||
if item.get("sign") in (None, "") and not isinstance(item.get("longitude"), (int, float)):
|
||||
continue
|
||||
label = item.get("name")
|
||||
if not isinstance(label, str) or "_" in label or not label.strip():
|
||||
label = " ".join(str(name).replace("_", " ").split())
|
||||
rows.append({
|
||||
"name": name,
|
||||
"status": item.get("formula_status") or item.get("formula_variant"),
|
||||
"value": item.get("base_longitude_before_exception") or item.get("longitude"),
|
||||
"name": label,
|
||||
"sign": item.get("sign"),
|
||||
"degree": item.get("degree_in_sign"),
|
||||
"longitude": item.get("longitude"),
|
||||
})
|
||||
return _rows_from(rows, language, [("name", "text"), ("status", "text"), ("value", "text")])
|
||||
return _rows_from(rows, language, [("name", "text"), ("sign", "sign"), ("degree", "text"), ("longitude", "text")])
|
||||
|
||||
|
||||
def _special_points(packet: dict, language: str) -> str:
|
||||
@@ -1361,31 +1467,30 @@ def _one_point(packet: dict, language: str, key: str) -> str:
|
||||
item = jaimini.get(key) if isinstance(jaimini, dict) else None
|
||||
if isinstance(item, str):
|
||||
return _rows_from([{"planet": item}], language, [("planet", "planet")])
|
||||
if isinstance(item, dict):
|
||||
return _rows_from([item], language, [("planet", "planet"), ("sign", "sign"), ("house", "text")])
|
||||
return _blank(language, ["planet", "sign"])
|
||||
if not isinstance(item, dict):
|
||||
return _blank(language, ["planet", "sign"])
|
||||
if key == "karakamsha":
|
||||
return _rows_from([{
|
||||
"sign": item.get("karakamsha_sign") or item.get("sign"),
|
||||
"degree": item.get("karakamsha_degree") if "karakamsha_degree" in item else item.get("degree"),
|
||||
"lord": item.get("karakamsha_lord") or item.get("lord") or item.get("planet"),
|
||||
"house": item.get("house"),
|
||||
}], language, [("sign", "sign"), ("degree", "text"), ("lord", "planet"), ("house", "text")])
|
||||
if key == "darakaraka":
|
||||
return _rows_from([{
|
||||
"planet": item.get("dk_planet") or item.get("planet"),
|
||||
"sign": item.get("dk_sign") or item.get("sign"),
|
||||
"house": item.get("dk_house") if item.get("dk_house") is not None else item.get("house"),
|
||||
"degree": item.get("dk_degree") if item.get("dk_degree") is not None else item.get("degree"),
|
||||
"value": item.get("d9_sign"),
|
||||
"dignity": _dignity_token(item.get("d9_dignity") or item.get("dignity"), language),
|
||||
}], language, [("planet", "planet"), ("sign", "sign"), ("house", "text"), ("degree", "text"), ("value", "sign"), ("dignity", "text")])
|
||||
return _rows_from([item], language, [("planet", "planet"), ("sign", "sign"), ("house", "text")])
|
||||
|
||||
|
||||
def _rashi_tulya(packet: dict, language: str) -> str:
|
||||
module = _first(packet, ("advanced_systems", "rashi_tulya_navamsa")) or {}
|
||||
analysis = module.get("analysis") if isinstance(module, dict) else module
|
||||
if not isinstance(analysis, dict):
|
||||
return _blank(language, ["name", "value"])
|
||||
labels = {
|
||||
"lagna_sign": "上升星座" if language == "zh" else "Ascendant sign",
|
||||
"strength_score": "力量分" if language == "zh" else "Strength score",
|
||||
"weakness_score": "薄弱分" if language == "zh" else "Weakness score",
|
||||
}
|
||||
rows = []
|
||||
for key, label in labels.items():
|
||||
if key in analysis:
|
||||
rows.append({"name": label, "value": analysis.get(key)})
|
||||
planets = analysis.get("planets")
|
||||
if isinstance(planets, dict):
|
||||
for planet, item in planets.items():
|
||||
sign = item.get("sign") if isinstance(item, dict) else item
|
||||
rows.append({"name": planet, "value": sign})
|
||||
return _rows_from(rows, language, [("name", "text"), ("value", "text")])
|
||||
del packet
|
||||
return _missing_module(language, ["name", "value"])
|
||||
|
||||
|
||||
def _moon(packet: dict, language: str) -> str:
|
||||
@@ -1403,7 +1508,10 @@ def _moon(packet: dict, language: str) -> str:
|
||||
|
||||
def _sudarshana(packet: dict, language: str) -> str:
|
||||
module = _first(packet, ("divisional_and_special_charts", "sudarshana")) or {}
|
||||
points = module.get("reference_points") if isinstance(module, dict) else {}
|
||||
raw = module.get("raw") if isinstance(module, dict) and isinstance(module.get("raw"), dict) else {}
|
||||
points = raw.get("reference_points") if isinstance(raw.get("reference_points"), dict) else None
|
||||
if not isinstance(points, dict) and isinstance(module, dict):
|
||||
points = module.get("reference_points")
|
||||
rows = []
|
||||
labels = {
|
||||
"ascendant_lagna": ("Ascendant reference", "出生上升参考点"),
|
||||
@@ -1419,16 +1527,37 @@ def _sudarshana(packet: dict, language: str) -> str:
|
||||
|
||||
|
||||
def _transit(packet: dict, language: str) -> str:
|
||||
module = _first(packet, ("timing_and_predictive_systems", "transit_positions")) or _first(packet, ("advanced_systems", "transit_positions"))
|
||||
if not isinstance(module, dict):
|
||||
module = {}
|
||||
planets = module.get("planets") or module.get("positions") or module
|
||||
multi = _first(packet, ("timing_and_predictive_systems", "transit_multi_reference")) or {}
|
||||
analysis = multi.get("transit_analysis") if isinstance(multi, dict) else {}
|
||||
when = multi.get("target_date") if isinstance(multi, dict) else None
|
||||
overlay_root = _first(packet, ("advanced_systems", "nakshatra_dasha")) or {}
|
||||
overlay = overlay_root.get("transit_overlay") if isinstance(overlay_root, dict) else {}
|
||||
if not isinstance(analysis, dict):
|
||||
analysis = {}
|
||||
if not isinstance(overlay, dict):
|
||||
overlay = {}
|
||||
order = [planet for planet in (*analysis.keys(), *overlay.keys()) if planet in _PLANETS]
|
||||
seen = []
|
||||
for planet in order:
|
||||
if planet not in seen:
|
||||
seen.append(planet)
|
||||
rows = []
|
||||
if isinstance(planets, dict):
|
||||
for planet, item in planets.items():
|
||||
if isinstance(item, dict):
|
||||
rows.append({"planet": planet, "sign": item.get("sign"), "date": item.get("date") or module.get("date")})
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("date", "text")])
|
||||
for planet in seen:
|
||||
item = analysis.get(planet) if isinstance(analysis.get(planet), dict) else {}
|
||||
over = overlay.get(planet) if isinstance(overlay.get(planet), dict) else {}
|
||||
nakshatra = over.get("transit_nakshatra")
|
||||
if isinstance(nakshatra, str) and "_" in nakshatra:
|
||||
nakshatra = nakshatra.replace("_", " ")
|
||||
if not isinstance(nakshatra, str):
|
||||
nakshatra = None
|
||||
rows.append({
|
||||
"planet": planet,
|
||||
"sign": item.get("sign"),
|
||||
"degree": item.get("degree_in_sign"),
|
||||
"date": when if isinstance(when, (str, int, float)) else item.get("date"),
|
||||
"nakshatra": nakshatra,
|
||||
})
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("degree", "text"), ("date", "text"), ("nakshatra", "text")])
|
||||
|
||||
|
||||
def _multi_transit(packet: dict, language: str) -> str:
|
||||
@@ -1465,19 +1594,15 @@ def _kp(packet: dict, language: str) -> str:
|
||||
|
||||
|
||||
def _d9(packet: dict, language: str) -> str:
|
||||
varga = _first(packet, ("divisional_and_special_charts", "varga_full")) or {}
|
||||
chart = None
|
||||
if isinstance(varga, dict):
|
||||
for key in ("D9", "d9", "Navamsa", "navamsa"):
|
||||
if isinstance(varga.get(key), dict):
|
||||
chart = varga[key]
|
||||
break
|
||||
planets = chart.get("planets") if isinstance(chart, dict) else None
|
||||
chart = _varga_chart(packet, "D9_Navamsa", "D9", "d9", "Navamsa", "navamsa")
|
||||
dignity = chart.get("_dignity") if isinstance(chart.get("_dignity"), dict) else {}
|
||||
rows = []
|
||||
if isinstance(planets, dict):
|
||||
for planet, item in planets.items():
|
||||
if isinstance(item, dict):
|
||||
rows.append({"planet": planet, "sign": item.get("sign"), "dignity": item.get("dignity") or item.get("status")})
|
||||
for planet, item in _named_planets(chart):
|
||||
rows.append({
|
||||
"planet": planet,
|
||||
"sign": item.get("sign"),
|
||||
"dignity": _dignity_token(dignity.get(planet) or item.get("dignity") or item.get("status"), language),
|
||||
})
|
||||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("dignity", "text")])
|
||||
|
||||
|
||||
@@ -1486,13 +1611,19 @@ def _mapping(packet: dict, language: str) -> str:
|
||||
rows = []
|
||||
if isinstance(varga, dict):
|
||||
for name, chart in varga.items():
|
||||
if not isinstance(chart, dict):
|
||||
continue
|
||||
if any(token in str(name).upper() for token in ("D81", "D108", "D144")):
|
||||
continue
|
||||
sign = None
|
||||
if isinstance(chart, dict):
|
||||
asc = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {}
|
||||
sign = asc.get("sign") or chart.get("asc_sign") or chart.get("lagna_sign")
|
||||
rows.append({"name": name, "sign": sign})
|
||||
asc = chart.get("Ascendant") if isinstance(chart.get("Ascendant"), dict) else {}
|
||||
if not asc and isinstance(chart.get("ascendant"), dict):
|
||||
asc = chart.get("ascendant") or {}
|
||||
sign = asc.get("sign") or chart.get("asc_sign") or chart.get("lagna_sign")
|
||||
if sign in (None, "") and isinstance(asc.get("sign_idx"), int):
|
||||
sign = asc.get("sign_idx")
|
||||
if sign in (None, ""):
|
||||
continue
|
||||
rows.append({"name": _varga_label(str(name), chart), "sign": sign})
|
||||
return _rows_from(rows, language, [("name", "text"), ("sign", "sign")])
|
||||
|
||||
|
||||
@@ -1515,7 +1646,9 @@ def _chalit_planets(packet: dict, language: str) -> str:
|
||||
def _chalit_centres(packet: dict, language: str) -> str:
|
||||
raw = _bhava_raw(packet)
|
||||
boundaries = raw.get("boundaries") if isinstance(raw.get("boundaries"), dict) else {}
|
||||
details = boundaries.get("houses_detail") if isinstance(boundaries, dict) else None
|
||||
details = boundaries.get("houses_detail") if isinstance(boundaries.get("houses_detail"), list) else None
|
||||
if not details:
|
||||
details = boundaries.get("houses") if isinstance(boundaries.get("houses"), list) else []
|
||||
rows = []
|
||||
for item in details or []:
|
||||
if isinstance(item, dict):
|
||||
@@ -1625,7 +1758,7 @@ def _render_named(packet: dict, language: str, title: str) -> str:
|
||||
"Tara-Chandra Matrix": lambda: _tara(packet, language),
|
||||
"Natal Tithi Lord": lambda: _tithi(packet, language),
|
||||
"Birth Calendar": lambda: _panchanga(packet, language),
|
||||
"Pancha Pakshi": lambda: _simple_module(packet, ("advanced_systems", "pancha_pakshi"), language, [("name", "text"), ("value", "text"), ("status", "text")]),
|
||||
"Pancha Pakshi": lambda: _missing_module(language, ["name", "value", "status"]),
|
||||
"Natal Vivah Saham": lambda: _vivah(packet, language),
|
||||
"Natal Saham Formula Basis": lambda: _saham_basis(packet, language),
|
||||
"Arudha / UL / A10": lambda: _special_points(packet, language),
|
||||
|
||||
@@ -278,6 +278,9 @@ def render_english(index: int) -> dict:
|
||||
edition = _english_edition(packet, True)
|
||||
chinese = render_full_data_markdown(packet)
|
||||
gates = chinese_prose_gates(chinese)
|
||||
from scripts.report_render.appendix_language import snapshot_appendix_drop_log
|
||||
|
||||
drops = snapshot_appendix_drop_log()
|
||||
markdown = edition.get("markdown_en") or ""
|
||||
flags = _flags_from_packet(packet)
|
||||
applicability = edition.get("reader_dasha_applicability_en") or ""
|
||||
@@ -298,6 +301,7 @@ def render_english(index: int) -> dict:
|
||||
"missing": rendered.count("[translation missing:"),
|
||||
"zh_english_signs": gates["english_signs"],
|
||||
"zh_han_function_glue": gates["han_function_glue"],
|
||||
"appendix_drops": drops,
|
||||
**_guard_snapshot(),
|
||||
**flags,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ are separate headings. Fictional charts only.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
|
||||
import pytest
|
||||
@@ -142,6 +143,108 @@ def test_three_fictional_charts_render_the_same_chapters(fictional_reports: dict
|
||||
assert "blocked" not in zh.lower()
|
||||
|
||||
|
||||
_UNDETERMINED = {"zh": "未判定", "en": "Not determined"}
|
||||
_GAP_LINE = {
|
||||
"zh": "公开报告资料包未带此模块",
|
||||
"en": "This module is not in the public report packet",
|
||||
}
|
||||
# These four modules are absent from the public worksheet. The reason is why
|
||||
# the section may stay undetermined. Anything else must show a real cell.
|
||||
_PACKET_GAPS = {
|
||||
"Navamsha AD Duration": "navamsha_dasha is computed on the reading but is not copied into the public worksheet",
|
||||
"Pancha Pakshi": "pancha_pakshi is not copied into the public worksheet",
|
||||
"Rashi Tulya Navamsa": "rashi_tulya_navamsa is not copied into the public worksheet",
|
||||
"Current Dasha Convergence": "dasa_convergence per-domain rows are not copied into the public worksheet",
|
||||
}
|
||||
|
||||
|
||||
def _table_blocks(body: str) -> list[list[list[str]]]:
|
||||
blocks = []
|
||||
current: list[str] = []
|
||||
for line in body.splitlines():
|
||||
if line.startswith("|"):
|
||||
current.append(line)
|
||||
continue
|
||||
if current:
|
||||
blocks.append(current)
|
||||
current = []
|
||||
if current:
|
||||
blocks.append(current)
|
||||
parsed = []
|
||||
for block in blocks:
|
||||
rows = []
|
||||
for index, line in enumerate(block):
|
||||
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
||||
if cells and all(set(cell) <= set("-: ") and cell for cell in cells):
|
||||
continue
|
||||
if index == 0:
|
||||
continue
|
||||
rows.append(cells)
|
||||
parsed.append(rows)
|
||||
return parsed
|
||||
|
||||
|
||||
def _section_has_real_cell(body: str, undetermined: str) -> bool:
|
||||
blocks = _table_blocks(body)
|
||||
if blocks:
|
||||
return any(
|
||||
cell and cell != undetermined
|
||||
for rows in blocks
|
||||
for row in rows
|
||||
for cell in row
|
||||
)
|
||||
prose = [line.strip() for line in body.splitlines() if line.strip()]
|
||||
return any(line and line != undetermined for line in prose)
|
||||
|
||||
|
||||
def test_every_section_has_a_real_cell_except_the_four_public_gaps(fictional_reports: dict[int, tuple[str, str]]) -> None:
|
||||
titles = dict(SECTIONS)
|
||||
for reason in _PACKET_GAPS.values():
|
||||
assert reason
|
||||
for index, (chinese, english) in fictional_reports.items():
|
||||
for language, markdown in (("zh", chinese), ("en", english)):
|
||||
chapter = _chapter_text(markdown, language)
|
||||
for title_en, title_zh in SECTIONS:
|
||||
heading = title_zh if language == "zh" else title_en
|
||||
body = _section_body(chapter, heading)
|
||||
if title_en in _PACKET_GAPS:
|
||||
first = next(line.strip() for line in body.splitlines() if line.strip())
|
||||
assert first == _GAP_LINE[language], f"{index} {heading}"
|
||||
continue
|
||||
assert _section_has_real_cell(body, _UNDETERMINED[language]), f"{index} {heading}"
|
||||
|
||||
|
||||
def test_parity_chapters_go_through_local_conventions() -> None:
|
||||
from scripts.pl9_full_data_export import _apply_local_report_conventions
|
||||
|
||||
packet = {
|
||||
"report_language": "zh",
|
||||
"worksheets": {
|
||||
"strengths_and_scores": {
|
||||
"vimsopaka": {
|
||||
"Sun": {"planet": "Sun", "total_score": 12.5, "category": "必然"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
chinese = render_full_data_markdown(packet)
|
||||
zh_body = _section_body(chinese, "Vimsopaka 分盘贡献")
|
||||
zh_replaced = _apply_local_report_conventions("必然", language="zh")
|
||||
assert zh_replaced == "可能"
|
||||
assert zh_replaced in zh_body
|
||||
assert "必然" not in zh_body
|
||||
|
||||
english_packet = copy.deepcopy(packet)
|
||||
english_packet["report_language"] = "en"
|
||||
english_packet["worksheets"]["strengths_and_scores"]["vimsopaka"]["Sun"]["category"] = "divorce"
|
||||
english = render_full_data_markdown(english_packet)
|
||||
en_body = _section_body(english, "Vimsopaka Contribution")
|
||||
en_replaced = _apply_local_report_conventions("divorce", language="en")
|
||||
assert en_replaced == "relationship strain"
|
||||
assert en_replaced in en_body
|
||||
assert "divorce" not in en_body.lower()
|
||||
|
||||
|
||||
def test_chart_nine_partial_kaal_and_friendship(fictional_reports: dict[int, tuple[str, str]]) -> None:
|
||||
chinese, english = fictional_reports[9]
|
||||
assert _KAAL_ZH.search(chinese)
|
||||
|
||||
@@ -3,24 +3,109 @@
|
||||
This file is not on the quick gate. Windows timing is recorded in
|
||||
docs/tasks/PROGRESS-report-english-han-leak-20261021.md. Set
|
||||
REPORT_ENGLISH_SWEEP_DUMP to a directory to write the English markdown for
|
||||
the acceptance pass. Workers stay at 4.
|
||||
the acceptance pass. Workers stay at 4. Set J4_DROP_STATS to a json path to
|
||||
write the Chinese-appendix drop counts from the same run.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
||||
from tests.report_english_fictional import CHART_COUNT, init_english_worker, render_english, row_problem
|
||||
|
||||
|
||||
def _j4_summary(rows: list[dict]) -> dict:
|
||||
annual_names: dict[str, int] = {}
|
||||
annual_charts = []
|
||||
annual_tables = 0
|
||||
effect_entries = 0
|
||||
effect_calls = 0
|
||||
effect_charts = []
|
||||
effect_keys: dict[str, int] = {}
|
||||
combination_entries = 0
|
||||
combination_calls = 0
|
||||
combination_charts = []
|
||||
combination_keys: dict[str, int] = {}
|
||||
replaced_entries = 0
|
||||
replaced_charts = []
|
||||
replaced_keys: dict[str, int] = {}
|
||||
for row in rows:
|
||||
drops = row.get("appendix_drops") if isinstance(row.get("appendix_drops"), dict) else {}
|
||||
annual = drops.get("annual_columns") if isinstance(drops.get("annual_columns"), dict) else {}
|
||||
tables = drops.get("annual_tables") if isinstance(drops.get("annual_tables"), dict) else {}
|
||||
effects = drops.get("effects") if isinstance(drops.get("effects"), dict) else {}
|
||||
combinations = drops.get("combinations_undetermined") if isinstance(drops.get("combinations_undetermined"), dict) else {}
|
||||
replaced = drops.get("combinations_replaced") if isinstance(drops.get("combinations_replaced"), dict) else {}
|
||||
if annual:
|
||||
annual_charts.append(row.get("i"))
|
||||
for name, count in annual.items():
|
||||
annual_names[name] = annual_names.get(name, 0) + int(count)
|
||||
annual_tables += int(tables.get("seen") or 0)
|
||||
if effects:
|
||||
effect_charts.append(row.get("i"))
|
||||
effect_entries += len(effects)
|
||||
effect_calls += sum(int(count) for count in effects.values())
|
||||
for key, count in effects.items():
|
||||
effect_keys[key] = effect_keys.get(key, 0) + int(count)
|
||||
if combinations:
|
||||
combination_charts.append(row.get("i"))
|
||||
combination_entries += len(combinations)
|
||||
combination_calls += sum(int(count) for count in combinations.values())
|
||||
for key, count in combinations.items():
|
||||
combination_keys[key] = combination_keys.get(key, 0) + int(count)
|
||||
if replaced:
|
||||
replaced_charts.append(row.get("i"))
|
||||
replaced_entries += len(replaced)
|
||||
for key, count in replaced.items():
|
||||
replaced_keys[key] = replaced_keys.get(key, 0) + int(count)
|
||||
return {
|
||||
"charts": len(rows),
|
||||
"annual_columns": annual_names,
|
||||
"annual_chart_count": len(annual_charts),
|
||||
"annual_tables_seen": annual_tables,
|
||||
"effect_entries": effect_entries,
|
||||
"effect_calls": effect_calls,
|
||||
"effect_chart_count": len(effect_charts),
|
||||
"effect_keys": effect_keys,
|
||||
"combination_undetermined_entries": combination_entries,
|
||||
"combination_undetermined_calls": combination_calls,
|
||||
"combination_undetermined_chart_count": len(combination_charts),
|
||||
"combination_undetermined_keys": combination_keys,
|
||||
"combination_replaced_entries": replaced_entries,
|
||||
"combination_replaced_chart_count": len(replaced_charts),
|
||||
"combination_replaced_keys": replaced_keys,
|
||||
}
|
||||
|
||||
|
||||
def test_two_hundred_fictional_charts_have_clean_english() -> None:
|
||||
workers = min(4, os.cpu_count() or 1, CHART_COUNT)
|
||||
failures = []
|
||||
rows = []
|
||||
with ProcessPoolExecutor(max_workers=workers, initializer=init_english_worker) as pool:
|
||||
futures = [pool.submit(render_english, index) for index in range(CHART_COUNT)]
|
||||
for future in as_completed(futures):
|
||||
row = future.result()
|
||||
rows.append(row)
|
||||
problem = row_problem(row)
|
||||
if problem:
|
||||
failures.append(problem)
|
||||
summary = _j4_summary(rows)
|
||||
print("J4_APPENDIX_DROPS " + json.dumps({
|
||||
"annual_columns": summary["annual_columns"],
|
||||
"annual_chart_count": summary["annual_chart_count"],
|
||||
"annual_tables_seen": summary["annual_tables_seen"],
|
||||
"effect_entries": summary["effect_entries"],
|
||||
"effect_calls": summary["effect_calls"],
|
||||
"effect_chart_count": summary["effect_chart_count"],
|
||||
"combination_undetermined_entries": summary["combination_undetermined_entries"],
|
||||
"combination_undetermined_calls": summary["combination_undetermined_calls"],
|
||||
"combination_undetermined_chart_count": summary["combination_undetermined_chart_count"],
|
||||
"combination_replaced_entries": summary["combination_replaced_entries"],
|
||||
"combination_replaced_chart_count": summary["combination_replaced_chart_count"],
|
||||
}, ensure_ascii=False))
|
||||
stats_path = os.environ.get("J4_DROP_STATS")
|
||||
if stats_path:
|
||||
Path(stats_path).write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
assert failures == []
|
||||
|
||||
Reference in New Issue
Block a user