feat(report): English reader edition from the same packet; yoga combination English twins (E1/E2/E3 wiring)
- /api/professional_report_reference accepts languages [zh,en]; one packet, zh byte-identical, markdown_en + reader_dasha_applicability_en, or english_unavailable when a Chinese character would leak. - pl9_reader_english renders the parity volume with report_language=en and a fixed term table; yoga_engine carries combination_en / effects_en / strength_en. - 75 fictional charts: zero Han, same line count, same numbers outside yoga tables. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0c06ab1d78
commit
419e1f4d37
@@ -0,0 +1,183 @@
|
||||
"""English edition of the reader report (TASK-report-english-edition-20260929, E1/E2).
|
||||
|
||||
Three clearly fictional charts (day, night, southern hemisphere). For each, the
|
||||
Chinese reader is rendered from the same packet before and after the English
|
||||
render; the English reader must carry no Chinese, pass the reader leak
|
||||
patterns, and carry the same numbers in the same order outside the yoga tables
|
||||
(yoga names and combination notes are prose whose English forms number things
|
||||
the Chinese names do not, e.g. "Kalatra Dosha (7th Lord in 6/8/12)").
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.jyotish_engine import build_professional_report_reference_packet, cmd_full_reading
|
||||
from scripts.pl9_reader_english import HAN, han_leaks
|
||||
from scripts.pl9_reader_export import _dignity_label, _pl9_export_markdown_for_edition
|
||||
from scripts.professional_report_reference import (
|
||||
ProfessionalReportReferenceInputError,
|
||||
_export_args,
|
||||
build_professional_report_reference,
|
||||
)
|
||||
from scripts.yoga_engine import BiText, _bijoin, _combo, _planet
|
||||
from tests.test_report_reader_main import FICTIONAL, _leak_hits
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
NUMBER = re.compile(r"-?\d+(?:\.\d+)?")
|
||||
|
||||
CASES = {
|
||||
"day": dict(FICTIONAL),
|
||||
# Clearly fictional night birth, northern China-like coordinates.
|
||||
"night": {**FICTIONAL, "name": "虚构乙", "year": 1984, "month": 11, "day": 23, "hour": 23, "minute": 40,
|
||||
"lat": 39.9042, "lon": 116.4074, "tz": 8.0, "age": 42},
|
||||
# Clearly fictional southern-hemisphere birth.
|
||||
"south": {**FICTIONAL, "name": "虚构丙", "year": 1977, "month": 2, "day": 14, "hour": 6, "minute": 5,
|
||||
"lat": -33.8688, "lon": 151.2093, "tz": 11.0, "age": 49},
|
||||
}
|
||||
|
||||
|
||||
def _packet(case: dict) -> dict:
|
||||
full_reading = cmd_full_reading(type("Args", (), dict(case))())
|
||||
packet = dict(build_professional_report_reference_packet(full_reading, _export_args(dict(case)), []))
|
||||
packet["report_version"] = "pl9_personal_long_report.v3"
|
||||
return packet
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=sorted(CASES))
|
||||
def rendered(request):
|
||||
packet = _packet(CASES[request.param])
|
||||
zh_before = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
en = _pl9_export_markdown_for_edition(english, "reader_main")
|
||||
zh_after = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
return request.param, zh_before, en, zh_after
|
||||
|
||||
|
||||
def _numbers_outside_yoga_tables(markdown: str) -> list[str]:
|
||||
numbers: list[str] = []
|
||||
in_yoga_table = False
|
||||
for line in markdown.splitlines():
|
||||
if line.startswith("| Yoga | Category |") or line.startswith("| 瑜伽 | 类别 |"):
|
||||
in_yoga_table = True
|
||||
continue
|
||||
if in_yoga_table and not line.startswith("|"):
|
||||
in_yoga_table = False
|
||||
if not in_yoga_table:
|
||||
numbers.extend(NUMBER.findall(line))
|
||||
return numbers
|
||||
|
||||
|
||||
def test_chinese_edition_is_untouched_by_the_english_render(rendered) -> None:
|
||||
_, zh_before, _, zh_after = rendered
|
||||
assert zh_after == zh_before
|
||||
|
||||
|
||||
def test_english_edition_has_no_chinese(rendered) -> None:
|
||||
_, _, en, _ = rendered
|
||||
assert han_leaks(en) == []
|
||||
assert not HAN.search(en)
|
||||
|
||||
|
||||
def test_english_edition_passes_reader_leak_patterns(rendered) -> None:
|
||||
_, _, en, _ = rendered
|
||||
assert _leak_hits(en) == []
|
||||
assert en.startswith("# Vedic Astrology Chart Report\n")
|
||||
for heading in ("## Birth Data and Charts", "## Strength, Relationships and Ashtakavarga",
|
||||
"## Standard Dasha Tables", "## Saturn and KP", "## Annual Charts and Tajika",
|
||||
"## Bhavesh, Dosha and Yoga", "#### Planet Lords and Dignity"):
|
||||
assert heading in en
|
||||
assert "| Planet | Sign lord | Star lord | Sub lord | Sub-sub lord | Dignity | Strength ratio |" in en
|
||||
|
||||
|
||||
def test_both_editions_carry_the_same_numbers_in_order(rendered) -> None:
|
||||
_, zh, en, _ = rendered
|
||||
assert _numbers_outside_yoga_tables(en) == _numbers_outside_yoga_tables(zh)
|
||||
# Line for line, too: the English edition is the same volume, relabelled.
|
||||
assert len(en.splitlines()) == len(zh.splitlines())
|
||||
|
||||
|
||||
def test_english_dignity_takes_the_english_half() -> None:
|
||||
assert _dignity_label("入旺(Exalted)", "en") == "Exalted"
|
||||
assert _dignity_label("落陷取消(Neecha Bhanga)", "en") == "Neecha Bhanga"
|
||||
assert _dignity_label("入旺(Exalted)") == "入旺"
|
||||
|
||||
|
||||
def test_yoga_combination_text_carries_an_english_twin() -> None:
|
||||
combo = _combo({}, "{a}与{b}同在第{h}宫", a=_planet("Sun"), b=_planet("Mercury"), h=10)
|
||||
assert combo == "太阳Sun与水星Mercury同在第10宫"
|
||||
assert isinstance(combo, BiText) and combo.en == "Sun and Mercury together in house 10"
|
||||
own = _combo({"combo_template": "{p}入庙", "combo_template_en": "{p} in own sign"}, "{p}在第{h}宫", p=_planet("Mars"))
|
||||
assert (str(own), own.en) == ("火星Mars入庙", "Mars in own sign")
|
||||
joined = _bijoin("; ", [combo, "", own])
|
||||
assert str(joined) == "太阳Sun与水星Mercury同在第10宫; 火星Mars入庙"
|
||||
assert joined.en == "Sun and Mercury together in house 10; Mars in own sign"
|
||||
|
||||
|
||||
def _api_response(packet: dict, body: dict) -> dict:
|
||||
class Handler:
|
||||
def _high_rigor_birth_payload(self, request):
|
||||
return {**FICTIONAL}
|
||||
|
||||
def _compute_full_reading_for_thematic(self, birth):
|
||||
return {"stub": True}
|
||||
|
||||
class Engine:
|
||||
def build_professional_report_reference_packet(self, full_reading, args, packs):
|
||||
return copy.deepcopy(packet)
|
||||
|
||||
return build_professional_report_reference(Handler(), body, engine=Engine())
|
||||
|
||||
|
||||
def test_api_without_languages_is_unchanged_and_with_languages_adds_english() -> None:
|
||||
packet = _packet(CASES["day"])
|
||||
base_body = {"format": "markdown", "edition": "reader_main", "include_fact_tables": True}
|
||||
plain = _api_response(packet, base_body)
|
||||
assert not {"markdown_en", "reader_dasha_applicability_en", "english_unavailable"} & set(plain)
|
||||
both = _api_response(packet, {**base_body, "languages": ["zh", "en"]})
|
||||
assert both["markdown"] == plain["markdown"]
|
||||
assert both["fact_table_packet"] == plain["fact_table_packet"]
|
||||
assert both["reader_dasha_applicability"] == plain["reader_dasha_applicability"]
|
||||
assert not HAN.search(both["markdown_en"])
|
||||
assert not HAN.search(both["reader_dasha_applicability_en"])
|
||||
assert "english_unavailable" not in both
|
||||
zh_only = _api_response(packet, {**base_body, "languages": ["zh"]})
|
||||
assert json.dumps(zh_only, sort_keys=True, ensure_ascii=False) == json.dumps(plain, sort_keys=True, ensure_ascii=False)
|
||||
|
||||
|
||||
def test_api_drops_english_when_chinese_would_leak(monkeypatch) -> None:
|
||||
from scripts import pl9_reader_english
|
||||
|
||||
packet = _packet(CASES["day"])
|
||||
monkeypatch.setattr(pl9_reader_english, "PHRASES_EN", [])
|
||||
reply = _api_response(packet, {"format": "markdown", "edition": "reader_main", "languages": ["zh", "en"]})
|
||||
assert reply["english_unavailable"] == "han_leak"
|
||||
assert "markdown_en" not in reply
|
||||
assert reply["markdown"]
|
||||
|
||||
|
||||
def test_api_rejects_languages_outside_reader_main() -> None:
|
||||
packet = {"report_version": "x"}
|
||||
with pytest.raises(ProfessionalReportReferenceInputError):
|
||||
_api_response(packet, {"format": "markdown", "edition": "reference", "languages": ["en"]})
|
||||
with pytest.raises(ProfessionalReportReferenceInputError):
|
||||
_api_response(packet, {"format": "markdown", "edition": "reader_main", "languages": ["fr"]})
|
||||
|
||||
|
||||
def test_english_golden_pair_matches_a_live_render() -> None:
|
||||
"""The frontend fixtures are a real zh/en pair from one packet (AGENTS §7-4)."""
|
||||
en_fixture = json.loads((ROOT / "frontend/tests/fixtures/report-reader-main-fictional-en.json").read_text(encoding="utf-8"))
|
||||
zh_fixture = json.loads((ROOT / "frontend/tests/fixtures/report-reader-main-fictional-zh-pair.json").read_text(encoding="utf-8"))
|
||||
assert en_fixture["fixtureProvenance"]["fictional"] is True
|
||||
assert en_fixture["fixtureProvenance"]["language"] == "en"
|
||||
assert zh_fixture["fixtureProvenance"]["language"] == "zh"
|
||||
assert en_fixture["fixtureProvenance"]["pairSha256"] == hashlib.sha256(zh_fixture["markdown"].encode("utf-8")).hexdigest()
|
||||
assert not HAN.search(en_fixture["markdown"])
|
||||
assert _numbers_outside_yoga_tables(en_fixture["markdown"]) == _numbers_outside_yoga_tables(zh_fixture["markdown"])
|
||||
Reference in New Issue
Block a user