From adfa98bf97bc94e44b67aba0777095a4e3b8c993 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 22 Oct 2026 09:58:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8B=B1=E6=96=87=E6=BC=8F=E8=AF=91=E6=94=B9?= =?UTF-8?q?=E5=9C=A8=E6=B8=B2=E6=9F=93=E4=BE=A7=E6=9F=A5=E8=A1=A8=EF=BC=8C?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=B8=8D=E5=86=8D=E6=94=BE=E8=A1=8C=20PyJHor?= =?UTF-8?q?a=20=E5=AD=90=E8=BF=9B=E7=A8=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 冻结的 jaimini.py 恢复成 staging 原样。缺译单元格写成固定标记,不再留空。 Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor --- scripts/jaimini.py | 12 -- scripts/pl9_full_data_export.py | 38 +++-- scripts/pl9_reader_export.py | 59 +++++-- scripts/report_render/english_gaps.py | 62 +++++++ scripts/report_render/karaka_domains.py | 16 ++ scripts/run_quality_gate.py | 2 +- tests/report_english_fictional.py | 202 +++++++++++++++++++---- tests/test_narayana_legacy_label.py | 3 + tests/test_report_english_han_sources.py | 49 ++++++ tests/test_report_english_sweep.py | 11 +- tests/test_report_english_sweep_smoke.py | 13 +- 11 files changed, 378 insertions(+), 89 deletions(-) create mode 100644 scripts/report_render/english_gaps.py create mode 100644 scripts/report_render/karaka_domains.py diff --git a/scripts/jaimini.py b/scripts/jaimini.py index 351733de..e6fb909a 100644 --- a/scripts/jaimini.py +++ b/scripts/jaimini.py @@ -75,16 +75,6 @@ KARAKA_DOMAINS = { 'Darakaraka': '配偶特质、伴侣关系、婚姻质量', 'Pitrukaraka': '父亲、祖先业力、传统传承', } -KARAKA_DOMAINS_EN = { - 'Atmakaraka': 'soul aim, core self, highest aim', - 'Amatyakaraka': 'career direction, main advisor, delegated authority', - 'Bhratrikaraka': 'siblings, courage, appetite for risk', - 'Matrikaraka': 'mother, home base, emotional security', - 'Putrakaraka': 'children, creativity, students, crafted results', - 'Gnatikaraka': 'rivals, illness, obstacles, capacity for change', - 'Darakaraka': 'spouse traits, partnership, marriage quality', - 'Pitrukaraka': 'father, ancestral pattern, inherited tradition', -} ARUDHA_NAMES = { 1: 'Arudha Lagna (AL)', 2: 'Dhana Pada (A2)', 3: 'Vikrama Pada (A3)', @@ -129,7 +119,6 @@ def calc_chara_karaka_7(planet_degrees: Dict[str, float]) -> Dict: 'degree_in_sign': round(deg, 4), 'rank': rank, 'domain': KARAKA_DOMAINS.get(karaka, ''), - 'domain_en': KARAKA_DOMAINS_EN.get(karaka, ''), 'cn_name': KARAKA_CN.get(karaka, karaka), } ak = results.get('Atmakaraka', {}) @@ -171,7 +160,6 @@ def calc_chara_karaka_8(planet_degrees: Dict[str, float]) -> Dict: 'rahu_reverse_applied': pname == 'Rahu', 'rank': rank, 'domain': KARAKA_DOMAINS.get(karaka, ''), - 'domain_en': KARAKA_DOMAINS_EN.get(karaka, ''), 'cn_name': KARAKA_CN.get(karaka, karaka), } return {'karaka_table_8': results} diff --git a/scripts/pl9_full_data_export.py b/scripts/pl9_full_data_export.py index a44145eb..97566864 100644 --- a/scripts/pl9_full_data_export.py +++ b/scripts/pl9_full_data_export.py @@ -2072,22 +2072,20 @@ def _render_pl9_ai_density_raw_data_markdown_en(packet: dict) -> str: english_report = _pl9_report_language(packet) == "en" strength_en_map: dict[str, str] = {} - han_leaks = None if english_report: try: from yoga_engine import STRENGTH_EN as strength_en_map except ImportError: from scripts.yoga_engine import STRENGTH_EN as strength_en_map try: - from pl9_reader_english import HAN as han_leaks + from report_render.english_gaps import english_effects, first_clean_english except ImportError: - from scripts.pl9_reader_english import HAN as han_leaks + from scripts.report_render.english_gaps import english_effects, first_clean_english - def _english_plain(value: Any) -> str: - text = str(value or "").strip() - if not text or (han_leaks is not None and han_leaks.search(text)): - return "" - return text + def _english_plain(value: Any, field: str, chinese: Any = None, fallback: Any = None) -> str: + if not english_report: + return str(value or "").strip() + return first_clean_english(value, fallback, field=field, chinese=chinese) yoga = dict_get(("advanced_systems", "yoga", "detected_yogas"), []) if isinstance(yoga, list): @@ -2095,12 +2093,18 @@ def _render_pl9_ai_density_raw_data_markdown_en(packet: dict) -> str: for item in yoga: if isinstance(item, dict): if english_report: - effects_src = item.get("effects_en") if isinstance(item.get("effects_en"), list) else [] - effects = [text for text in (_english_plain(effect) for effect in effects_src) if text] - combination = _english_plain(item.get("combination_en")) or "-" - strength = _english_plain(item.get("strength_en")) - if not strength: - strength = _english_plain(strength_en_map.get(str(item.get("strength") or ""), "")) + effects = english_effects(item.get("effects_en"), item.get("effects")) + combination = _english_plain( + item.get("combination_en"), + "combination", + item.get("combination"), + ) or "-" + strength = _english_plain( + item.get("strength_en"), + "strength", + item.get("strength"), + strength_en_map.get(str(item.get("strength") or ""), ""), + ) or "-" else: effects = [effect for effect in (clean_effect(e) for e in item.get("effects", [])) if effect] combination = item.get("combination") @@ -2122,7 +2126,11 @@ def _render_pl9_ai_density_raw_data_markdown_en(packet: dict) -> str: item = dosha.get(key) if isinstance(item, dict): if english_report: - summary = _english_plain(item.get("summary_en")) or "-" + summary = _english_plain( + item.get("summary_en"), + "summary", + item.get("summary"), + ) or "-" else: summary = item.get("summary") or item.get("phase") or item.get("type") rows.append([ diff --git a/scripts/pl9_reader_export.py b/scripts/pl9_reader_export.py index 89deb831..33295961 100644 --- a/scripts/pl9_reader_export.py +++ b/scripts/pl9_reader_export.py @@ -26,6 +26,12 @@ try: # scripts/ on sys.path from narayana_legacy_label import LEGACY_NOTE_EN, LEGACY_NOTE_ZH except ImportError: # pragma: no cover - package import from scripts.narayana_legacy_label import LEGACY_NOTE_EN, LEGACY_NOTE_ZH +try: # scripts/ on sys.path + from report_render.english_gaps import english_effects, first_clean_english + from report_render.karaka_domains import KARAKA_DOMAIN_EN +except ImportError: # pragma: no cover - package import + from scripts.report_render.english_gaps import english_effects, first_clean_english + from scripts.report_render.karaka_domains import KARAKA_DOMAIN_EN import re SIGNS: list[str] = [] @@ -1573,9 +1579,14 @@ def render_pl9_parity_markdown(packet: dict) -> str: if not isinstance(row, dict): continue karaka_name = name if _lang == 'en' else (row.get('cn_name') or name) - domain = row.get('domain_en') if _lang == 'en' else row.get('domain') - if _lang == 'en' and domain and re.search(r'[\u4e00-\u9fff]', str(domain)): - domain = '' + if _lang == 'en': + domain = first_clean_english( + KARAKA_DOMAIN_EN.get(str(name), ''), + field='domain', + chinese=row.get('domain'), + ) + else: + domain = row.get('domain') lines.append( f"| {_cell(karaka_name)} | {_cell(_token(row.get('planet')))} | " f"{_cell(_degree(row.get('degree_in_sign') or row.get('sign_degree')))} | " @@ -2120,9 +2131,14 @@ def render_pl9_parity_markdown(packet: dict) -> str: # Customer tables retain computed boundaries and reference points; # print-layout rows deliberately blank repeated values. kantaka_rows = _list(timeline.get('kantaka_rows')) or _list(display_rows.get('kantaka')) - sade_summary = sade_sati.get('summary_en') if _lang == 'en' else sade_sati.get('summary') - if _lang == 'en' and sade_summary and re.search(r'[\u4e00-\u9fff]', str(sade_summary)): - sade_summary = '' + if _lang == 'en': + sade_summary = first_clean_english( + sade_sati.get('summary_en'), + field='summary', + chinese=sade_sati.get('summary'), + ) + else: + sade_summary = sade_sati.get('summary') lines.extend([ '### p121-p122 Sade Sati / Dhayya / Kantaka Saturn', '', @@ -3029,9 +3045,14 @@ def render_pl9_parity_markdown(packet: dict) -> str: for fact_key in ('mars_house', 'mars_sign', 'sun_house', 'sun_sign', 'rahu_house', 'ketu_house', 'moon_sign', 'severity', 'type'): if row.get(fact_key) not in (None, ''): fact_bits.append(f"{fact_key}={_token(row.get(fact_key))}") - summary = row.get('summary_en') if _lang == 'en' else row.get('summary') - if _lang == 'en' and summary and re.search(r'[\u4e00-\u9fff]', str(summary)): - summary = '' + if _lang == 'en': + summary = first_clean_english( + row.get('summary_en'), + field='summary', + chinese=row.get('summary'), + ) + else: + summary = row.get('summary') joiner = '; ' if _lang == 'en' else ';' lines.append( f"| {_cell(label)} | {_cell(detected)} | {_cell(joiner.join(map(str, fact_bits)) or '-')} | {_cell(summary)} |" @@ -3118,15 +3139,19 @@ def render_pl9_parity_markdown(packet: dict) -> str: ]) for row in group_rows: if _lang == 'en': - effects = row.get('effects_en') if isinstance(row.get('effects_en'), list) else [] - effects = [item for item in effects if not re.search(r'[\u4e00-\u9fff]', str(item))] + effects = english_effects(row.get('effects_en'), row.get('effects')) yoga_name = row.get('name') or row.get('type') - combination = row.get('combination_en') - if not combination or re.search(r'[\u4e00-\u9fff]', str(combination)): - combination = '' - strength = row.get('strength_en') or strength_en_map.get(str(row.get('strength') or '')) - if not strength or re.search(r'[\u4e00-\u9fff]', str(strength)): - strength = '' + combination = first_clean_english( + row.get('combination_en'), + field='combination', + chinese=row.get('combination'), + ) + strength = first_clean_english( + row.get('strength_en'), + strength_en_map.get(str(row.get('strength') or ''), ''), + field='strength', + chinese=row.get('strength'), + ) else: effects = row.get('effects') if isinstance(row.get('effects'), list) else [] yoga_name = row.get('name_cn') or row.get('name') or row.get('type') diff --git a/scripts/report_render/english_gaps.py b/scripts/report_render/english_gaps.py new file mode 100644 index 00000000..20e3a883 --- /dev/null +++ b/scripts/report_render/english_gaps.py @@ -0,0 +1,62 @@ +"""Marker for an English report cell that is still Chinese or has no English twin.""" + +from __future__ import annotations + +import re +from typing import Any + +HAN = re.compile(r"[\u4e00-\u9fff]") +MISSING_MARK = "[translation missing:" + + +def translation_missing(field: str) -> str: + return f"[translation missing: {field}]" + + +def _as_text(value: Any) -> str: + if value is None: + return "" + if isinstance(value, (list, tuple)): + return " ".join(part for part in (_as_text(item) for item in value) if part) + return str(value).strip() + + +def first_clean_english(*candidates: Any, field: str, chinese: Any = None) -> str: + """First candidate with no Han, or a visible marker. + + Empty English beside an empty Chinese source stays empty. A Han string, or + a Chinese source with no clean English candidate, is not blanked out. + """ + saw_text = False + for candidate in candidates: + text = _as_text(candidate) + if not text: + continue + saw_text = True + if HAN.search(text) is None: + return text + if saw_text or _as_text(chinese): + return translation_missing(field) + return "" + + +def english_effects(effects_en: Any, effects_zh: Any, field: str = "effects") -> list[str]: + """English effect lines. Han lines become one missing-translation marker.""" + source = effects_en if isinstance(effects_en, list) else [] + cleaned: list[str] = [] + dropped_han = False + for item in source: + text = _as_text(item) + if not text: + continue + if HAN.search(text) is not None: + dropped_han = True + continue + cleaned.append(text) + if dropped_han: + cleaned.append(translation_missing(field)) + if cleaned: + return cleaned + if _as_text(effects_zh) or _as_text(effects_en): + return [translation_missing(field)] + return [] diff --git a/scripts/report_render/karaka_domains.py b/scripts/report_render/karaka_domains.py new file mode 100644 index 00000000..a0100f50 --- /dev/null +++ b/scripts/report_render/karaka_domains.py @@ -0,0 +1,16 @@ +"""English Chara Karaka duties, keyed by karaka name. + +``scripts/jaimini.py`` is a sealed rectification scoring file. English wording +for the report is resolved here, at render time, and that file stays unchanged. +""" + +KARAKA_DOMAIN_EN = { + "Atmakaraka": "soul aim, core self, highest aim", + "Amatyakaraka": "career direction, main advisor, delegated authority", + "Bhratrikaraka": "siblings, courage, appetite for risk", + "Matrikaraka": "mother, home base, emotional security", + "Putrakaraka": "children, creativity, students, crafted results", + "Gnatikaraka": "rivals, illness, obstacles, capacity for change", + "Darakaraka": "spouse traits, partnership, marriage quality", + "Pitrukaraka": "father, ancestral pattern, inherited tradition", +} diff --git a/scripts/run_quality_gate.py b/scripts/run_quality_gate.py index 6c52d03a..61c3630b 100644 --- a/scripts/run_quality_gate.py +++ b/scripts/run_quality_gate.py @@ -135,7 +135,7 @@ CORE_PYTEST_TARGETS = [ "tests/test_serif_font_slices.py", # Chart, report, and consult entries must not import PyJHora (BUG-1273). "tests/test_runtime_jhora_import_scan.py", - # English edition han leak, 12-chart slice (issue #89 / BUG-1410). The 200-chart file stays off this list. + # English edition han leak, 12-chart slice (issue #89 / BUG-1413). The 200-chart file stays off this list. "tests/test_report_english_sweep_smoke.py", ] diff --git a/tests/report_english_fictional.py b/tests/report_english_fictional.py index 7ad8faa5..3f5771f9 100644 --- a/tests/report_english_fictional.py +++ b/tests/report_english_fictional.py @@ -83,8 +83,27 @@ def fictional_case(index: int) -> dict: return chosen +_JHORA_BLOCKED = False +_GUARD = { + "pyjhora_subprocess_runs": 0, + "subprocess_runs": 0, + "tajaka": None, + "annual": None, + "installed": False, +} +_PYJHORA_COMMAND_MARKERS = ( + "_isolated_tajaka_worker", + "_isolated_annual_worker", + "from jhora", + "import jhora", +) + + def block_jhora() -> None: """Fail closed if this process imports PyJHora. Call once per process.""" + global _JHORA_BLOCKED + if _JHORA_BLOCKED: + return real_import = builtins.__import__ def guarded(name, globals=None, locals=None, fromlist=(), level=0): @@ -104,6 +123,116 @@ def block_jhora() -> None: for key in list(sys.modules): if key == "jhora" or key.startswith("jhora."): del sys.modules[key] + _JHORA_BLOCKED = True + + +def _command_blob(args, kwargs) -> str: + command = args[0] if args else kwargs.get("args", ()) + if isinstance(command, str): + return command + if isinstance(command, (list, tuple)): + return " ".join(str(part) for part in command) + return str(command) + + +def _install_replay_stubs() -> None: + """Return the no-PyJHora blocked shape without starting the isolated children. + + Production still calls the real ``_run_isolated_*`` functions. Only this + test process replaces them, including workers started by the process pool. + """ + import subprocess + + import scripts.annual_pyjhora_replay as replay + try: + import annual_pyjhora_replay as replay_bare + except ImportError: + replay_bare = replay + + if not getattr(subprocess.run, "_english_sweep_counter", False): + real_run = subprocess.run + + def counting_run(*args, **kwargs): + blob = _command_blob(args, kwargs) + _GUARD["subprocess_runs"] += 1 + if any(marker in blob for marker in _PYJHORA_COMMAND_MARKERS): + _GUARD["pyjhora_subprocess_runs"] += 1 + return real_run(*args, **kwargs) + + counting_run._english_sweep_counter = True + subprocess.run = counting_run + + if _GUARD["installed"]: + return + + def blocked_tajaka(profile, birth_julian_day, place, age): + settings = replay._replay_settings(profile) + request = { + "birth_julian_day": birth_julian_day, + "place": replay._place_payload(place), + "age": age, + "settings": settings, + } + result = replay._no_jhora_tajaka_replay(settings) + result["request_hash"] = replay._request_hash(request) + _GUARD["tajaka"] = result + return result + + def blocked_annual(profile, birth_julian_day, place, age): + settings = replay._replay_settings(profile) + request = { + "birth_julian_day": birth_julian_day, + "place": replay._place_payload(place), + "age": age, + "settings": settings, + } + result = replay._no_jhora_annual_replay(settings) + result["request_hash"] = replay._request_hash(request) + _GUARD["annual"] = result + return result + + for module in {replay, replay_bare}: + module._run_isolated_tajaka_year_lord = blocked_tajaka + module._run_isolated_annual_replay = blocked_annual + _GUARD["installed"] = True + + +def _reset_guard_counts() -> None: + _GUARD["pyjhora_subprocess_runs"] = 0 + _GUARD["subprocess_runs"] = 0 + _GUARD["tajaka"] = None + _GUARD["annual"] = None + + +def _guard_snapshot() -> dict: + from scripts.annual_pyjhora_replay import _ANNUAL_SURFACE_KEYS + + tajaka = _GUARD["tajaka"] + annual = _GUARD["annual"] + annual_reason = "pyjhora_annual_adapter_setup_failed:ModuleNotFoundError" + annual_blocked = ( + isinstance(annual, dict) + and isinstance(annual.get("request_hash"), str) + and bool(annual.get("request_hash")) + and all( + isinstance(annual.get(key), dict) + and annual[key].get("status") == "blocked" + and annual[key].get("reason") == annual_reason + for key in _ANNUAL_SURFACE_KEYS + ) + ) + tajaka_blocked = ( + isinstance(tajaka, dict) + and tajaka.get("status") == "blocked" + and tajaka.get("reason") == "pyjhora_ayanamsa_configuration_failed:ModuleNotFoundError" + and isinstance(tajaka.get("request_hash"), str) + and bool(tajaka.get("request_hash")) + ) + return { + "pyjhora_subprocess_runs": _GUARD["pyjhora_subprocess_runs"], + "tajaka_blocked": tajaka_blocked, + "annual_blocked": annual_blocked, + } def _prepare_process() -> None: @@ -112,6 +241,12 @@ def _prepare_process() -> None: if entry not in sys.path: sys.path.insert(0, entry) block_jhora() + _install_replay_stubs() + + +def init_english_worker() -> None: + """Process-pool startup. ``render_english`` installs the same guards again.""" + _prepare_process() def _residue(text: str) -> dict: @@ -120,16 +255,24 @@ def _residue(text: str) -> dict: return {"han": len(found), "han_chars": "".join(sorted(set(found)))[:40], "glue": glue} -def render_english(index: int) -> dict: - """Build one fictional packet and run ``_english_edition``. No birth data in the result.""" +def _load_packet(index: int) -> dict: _prepare_process() + _reset_guard_counts() from scripts.jyotish_engine import build_professional_report_reference_packet, cmd_full_reading - from scripts.professional_report_reference import _english_edition, _export_args + from scripts.professional_report_reference import _export_args birth = fictional_case(index) reading = cmd_full_reading(type("Args", (), dict(birth))()) packet = dict(build_professional_report_reference_packet(reading, _export_args(dict(birth)), [])) packet["report_version"] = "pl9_personal_long_report.v3" + return packet + + +def render_english(index: int) -> dict: + """Build one fictional packet and run ``_english_edition``. No birth data in the result.""" + from scripts.professional_report_reference import _english_edition + + packet = _load_packet(index) edition = _english_edition(packet, True) markdown = edition.get("markdown_en") or "" flags = _flags_from_packet(packet) @@ -141,12 +284,15 @@ def render_english(index: int) -> dict: path = Path(dump_dir) path.mkdir(parents=True, exist_ok=True) (path / f"chart-{index:03d}.md").write_text(markdown, encoding="utf-8") + rendered = f"{markdown}\n{applicability}" return { "i": index, "status": "ok" if markdown and not edition.get("english_unavailable") else edition.get("english_unavailable") or "empty", "han": body["han"] + extra["han"], "han_chars": (body["han_chars"] + extra["han_chars"])[:40], "glue": sorted(set(body["glue"] + extra["glue"])), + "missing": rendered.count("[translation missing:"), + **_guard_snapshot(), **flags, } @@ -173,33 +319,27 @@ def _flags_from_packet(packet: dict) -> dict: } +def row_problem(row: dict) -> str: + """Empty when this chart's English edition matches the no-PyJHora production report.""" + clean = ( + row["status"] == "ok" + and not row["han"] + and not row["glue"] + and not row["missing"] + and row["pyjhora_subprocess_runs"] == 0 + and row["tajaka_blocked"] + and row["annual_blocked"] + ) + if clean: + return "" + return ( + f"#{row['i']} status={row['status']} han={row['han']} chars={row['han_chars']} " + f"glue={row['glue']} missing={row['missing']} " + f"pyjhora_subprocess_runs={row['pyjhora_subprocess_runs']} " + f"tajaka_blocked={row['tajaka_blocked']} annual_blocked={row['annual_blocked']}" + ) + + def chart_flags(index: int) -> dict: """Class flags used to prove the smoke set covers L1–L6. No birth data.""" - _prepare_process() - from scripts.jyotish_engine import build_professional_report_reference_packet, cmd_full_reading - from scripts.professional_report_reference import _export_args - - birth = fictional_case(index) - reading = cmd_full_reading(type("Args", (), dict(birth))()) - packet = dict(build_professional_report_reference_packet(reading, _export_args(dict(birth)), [])) - rath = (((packet.get("worksheets") or {}).get("timing_and_predictive_systems") or {}).get("narayana_dasha") or {}).get("rath") or {} - rule = rath.get("current_md_antardasha_rule") or {} - direction = rule.get("direction") if isinstance(rule, dict) else {} - advanced = (packet.get("worksheets") or {}).get("advanced_systems") or {} - doshas = advanced.get("yogas_doshas") or {} - kaal = doshas.get("kaal_sarp_dosha") or {} - birth_sheet = (packet.get("worksheets") or {}).get("birth_and_parameters") or {} - pack = birth_sheet.get("panchanga") or {} - panchanga = pack.get("raw") if isinstance(pack, dict) and isinstance(pack.get("raw"), dict) else pack - yoga = advanced.get("yoga") if isinstance(advanced.get("yoga"), dict) else {} - names = [str(row.get("name") or "") for row in (yoga.get("detected_yogas") or []) if isinstance(row, dict)] - strengths = [str(row.get("strength") or "") for row in (yoga.get("detected_yogas") or []) if isinstance(row, dict)] - return { - "i": index, - "level": rath.get("strength_level_decided"), - "direction": rath.get("direction"), - "ad_rule": direction.get("rule") if isinstance(direction, dict) else None, - "overall": panchanga.get("overall_quality") if isinstance(panchanga, dict) else None, - "kaal": kaal.get("type") if isinstance(kaal, dict) else None, - "punarphoo": "观察" in strengths or any("Punarphoo" in name for name in names), - } + return {"i": index, **_flags_from_packet(_load_packet(index))} diff --git a/tests/test_narayana_legacy_label.py b/tests/test_narayana_legacy_label.py index 75536d06..8566df64 100644 --- a/tests/test_narayana_legacy_label.py +++ b/tests/test_narayana_legacy_label.py @@ -94,6 +94,9 @@ def test_reference_edition_shows_both_and_english_terms_cover_the_rath_lines() - from scripts.narayana_legacy_label import rath_report_lines_en packet = _packet(CASES["day"]) + # 原值:reference 版同时有旧算法句和 Rath 表 + # 新值:full_data 版同时有这两段 + # 原因:用户看到的是完整数据版;Rath 并列表必须跟着走(BUG-1218) # 原值:Rath 中文行经 PHRASES_EN 可译干净 # 新值:完整数据版英文渲染的 Rath 段无汉字且用英文函数 # 原因:完整数据版不走 PHRASES_EN,旧断言测的不是生产路径 diff --git a/tests/test_report_english_han_sources.py b/tests/test_report_english_han_sources.py index bbbde0f8..f7a3e4a3 100644 --- a/tests/test_report_english_han_sources.py +++ b/tests/test_report_english_han_sources.py @@ -3,6 +3,8 @@ from __future__ import annotations import re +from pathlib import Path + from scripts.muhurta import calc_panchanga from scripts.narayana_legacy_label import ( DIRECTION_EN, @@ -146,3 +148,50 @@ def test_punarphoo_strength_column_uses_english() -> None: assert "| Punarphoo Yoga | relationship_observation | observational | Moon conjunct Saturn |" in text assert "观察" not in text assert STRENGTH_EN["观察"] == "Watch" + assert "[translation missing:" not in text + + +def test_missing_english_twin_is_marked_instead_of_blank() -> None: + packet = { + "report_language": "en", + "worksheets": {"advanced_systems": {"yoga": {"detected_yogas": [{ + "name": "Example Yoga", + "category": "special", + "strength": "强", + "combination": "月亮与土星合相", + "effects": ["中文作用"], + }]}}}, + } + text = render_pl9_parity_markdown(packet) + assert "[translation missing: combination]" in text + assert "[translation missing: effects]" in text + assert "月亮" not in text + assert "中文" not in text + assert "| Strong |" in text or "| strong |" in text or "Strong" in text + + +def test_jaimini_domain_is_looked_up_by_karaka_name() -> None: + source = (Path(__file__).resolve().parents[1] / "scripts" / "jaimini.py").read_text(encoding="utf-8") + assert "domain_en" not in source + assert "KARAKA_DOMAINS_EN" not in source + packet = { + "report_language": "en", + "worksheets": { + "strengths_and_scores": {"scope": "jaimini-domain-fixture"}, + "advanced_systems": {"jaimini": {"chara_karaka_7": {"karaka_table": { + "Amatyakaraka": { + "planet": "Saturn", + "degree_in_sign": 1.25, + "effective_degree_in_sign": 1.25, + "domain": "事业方向、主要谋士、权力代理", + "cn_name": "事业星AmK", + }, + }}}}, + }, + } + text = render_pl9_parity_markdown(packet) + assert "career direction, main advisor, delegated authority" in text + assert "事业" not in text + assert "careerdirection" not in text + assert "careerplanetAmK" not in text + assert "[translation missing:" not in text diff --git a/tests/test_report_english_sweep.py b/tests/test_report_english_sweep.py index b7839bd8..4b3d4309 100644 --- a/tests/test_report_english_sweep.py +++ b/tests/test_report_english_sweep.py @@ -10,18 +10,17 @@ from __future__ import annotations import os from concurrent.futures import ProcessPoolExecutor, as_completed -from tests.report_english_fictional import CHART_COUNT, render_english +from tests.report_english_fictional import CHART_COUNT, init_english_worker, render_english, row_problem def test_two_hundred_fictional_charts_have_clean_english() -> None: workers = min(4, os.cpu_count() or 1, CHART_COUNT) failures = [] - with ProcessPoolExecutor(max_workers=workers) as pool: + 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() - if row["status"] != "ok" or row["han"] or row["glue"]: - failures.append( - f"#{row['i']} status={row['status']} han={row['han']} chars={row['han_chars']} glue={row['glue']}" - ) + problem = row_problem(row) + if problem: + failures.append(problem) assert failures == [] diff --git a/tests/test_report_english_sweep_smoke.py b/tests/test_report_english_sweep_smoke.py index 3bc39894..dac6de95 100644 --- a/tests/test_report_english_sweep_smoke.py +++ b/tests/test_report_english_sweep_smoke.py @@ -1,4 +1,4 @@ -"""Smoke slice of the English sweep, on the quick gate (issue #89 / BUG-1410). +"""Smoke slice of the English sweep, on the quick gate (issue #89 / BUG-1413). Indices are from tests.report_english_fictional.fictional_case. Together they hit Rath level 5, an inauspicious Panchanga, Ketu in the start sign, a Table 14 @@ -9,7 +9,7 @@ from __future__ import annotations from concurrent.futures import ProcessPoolExecutor, as_completed -from tests.report_english_fictional import render_english +from tests.report_english_fictional import init_english_worker, render_english, row_problem # Filled once the flag scan names a chart for every class. The test checks the # flags, so a stale index fails instead of silently dropping a class. @@ -23,15 +23,14 @@ SMOKE_INDEXES = (1, 4, 5, 6, 9, 10, 11, 12, 14, 15, 32, 34) def test_smoke_charts_cover_every_leak_class_and_render_clean_english() -> None: rows = [] failures = [] - with ProcessPoolExecutor(max_workers=min(4, len(SMOKE_INDEXES))) as pool: + with ProcessPoolExecutor(max_workers=min(4, len(SMOKE_INDEXES)), initializer=init_english_worker) as pool: futures = [pool.submit(render_english, index) for index in SMOKE_INDEXES] for future in as_completed(futures): row = future.result() rows.append(row) - if row["status"] != "ok" or row["han"] or row["glue"]: - failures.append( - f"#{row['i']} status={row['status']} han={row['han']} chars={row['han_chars']} glue={row['glue']}" - ) + problem = row_problem(row) + if problem: + failures.append(problem) levels = {row["level"] for row in rows} directions = {row["direction"] for row in rows} ad_rules = {row["ad_rule"] for row in rows}