标出研究章和 PyJHora 的已知差别(#92)。

10 张虚构盘逐体系对照。不一致的方法在标题下写明差在哪一类。本命 Tithi 顺行一致,逆行没有公开函数。Windows 上 1970 年以前的日出不再用 fromtimestamp。

自测:合同与研究章测试通过;200 张英文扫描约 1956 秒通过。未跑全量 pytest、快速门和前端。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-23 22:29:52 +08:00
co-authored by Grok (agent loop)
parent e586886196
commit a757af6b4e
10 changed files with 1196 additions and 35 deletions
+17 -1
View File
@@ -88,7 +88,14 @@ def _class_args(**overrides):
def _leak_hits(markdown: str) -> list[str]:
return [pattern.pattern for pattern in LEAK_PATTERNS if pattern.search(markdown)]
# 原值:整篇里出现 PyJHora 都算泄漏
# 新值:以「与 PyJHora 不一致:」或「Differs from PyJHora:」开头的行先拿掉,其余照旧
# 原因:#92 研究章要在标题下保留这两句对照;其它 PyJHora 字样仍然失败
kept = "\n".join(
line for line in (markdown or "").splitlines()
if not line.startswith("与 PyJHora 不一致:") and not line.startswith("Differs from PyJHora:")
)
return [pattern.pattern for pattern in LEAK_PATTERNS if pattern.search(kept)]
@pytest.fixture(scope="module")
@@ -169,6 +176,15 @@ def test_class_args_copy_keeps_year_for_kp_and_transit(full_reading) -> None:
assert "error" not in module or not str(module.get("error", "")).startswith("KP producer failed")
def test_pyjhora_mismatch_lines_are_not_reader_leaks() -> None:
# 原值:任何含 PyJHora 的文本都命中泄漏
# 新值:只有对照句以外的 PyJHora 命中泄漏
# 原因:#92 允许研究章保留这两句,不能因此放行其它位置
assert _leak_hits("Differs from PyJHora: start\n与 PyJHora 不一致:差在 起点\n") == []
hits = _leak_hits("Calculated by PyJHora.\n")
assert any("PyJHora" in hit for hit in hits)
def test_reader_main_hides_audit_vocabulary(full_reading) -> None:
from scripts.jyotish_engine import build_professional_report_reference_packet
+102 -2
View File
@@ -6,6 +6,7 @@ import ast
import bisect
import hashlib
import json
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
@@ -16,7 +17,7 @@ from scripts.research_timing.bundle import chart_from_parts, research_bundle
from scripts.research_timing.frame import SiderealFrame, birth_jd
from scripts.research_timing.rashi_pd import attach_rashi_pratyantar
from scripts.research_timing.render import _heading, render_research_chapters
from scripts.research_timing.search import ElongationClock, find_returns
from scripts.research_timing.search import ElongationClock, _utc_from_jd, find_returns, hindu_solar_events
from scripts.rashi_dashas_native import calculate_rashi_dashas
from tests.report_english_fictional import fictional_case
@@ -125,7 +126,11 @@ def test_rendered_chapters_keep_the_research_banner_and_drop_snake_case() -> Non
assert _SNAKE.search(chinese) is None
assert _SNAKE.search(english) is None
assert "blocked" not in english.lower()
assert "PyJHora" not in english
# 原值:assert "PyJHora" not in english
# 新值:英文里的 PyJHora 只允许出现在「Differs from PyJHora:」开头的对照行
# 原因:#92 修复单要求不一致的体系写这句;其它位置仍然不能出现 PyJHora
stray = [line for line in english.splitlines() if "PyJHora" in line and not line.startswith("Differs from PyJHora:")]
assert stray == []
assert "主星/名称" in chinese
assert "次小运按简化模型,从现有主运按比例分出。主运和子运与原来的表相同。" in chinese
assert "收录的教程没有写明第三层。" in chinese
@@ -190,6 +195,101 @@ def test_rectification_modules_do_not_import_the_research_package() -> None:
assert all("research_timing" not in module for module in modules), path.name
def _sections(markdown: str) -> list[tuple[str, str]]:
parts = re.split(r"(?m)^## ", markdown)
rows = []
for part in parts[1:]:
title, _, body = part.partition("\n")
rows.append((title.strip(), body))
return rows
def _under(rows: list[tuple[str, str]], heading: str, match: str) -> list[str]:
found = []
for title, body in rows:
if title == heading or (match == "prefix" and title.startswith(heading + " ")):
found.append(body)
return found
def test_solar_events_before_1970_stay_available_on_windows() -> None:
"""BUG-1429. Windows fromtimestamp rejects instants before 1970."""
moment = datetime(1950, 6, 15, 12, 0, tzinfo=timezone.utc)
events = hindu_solar_events(moment, 0.0, 0.0)
assert events
assert all(item["event"] in {"sunrise", "sunset"} for item in events)
parsed = datetime.fromisoformat(events[0]["datetime_utc"])
assert parsed.year == 1950
assert parsed.tzinfo is not None
after = _utc_from_jd(2451545.0)
expect = datetime.fromtimestamp((2451545.0 - 2440587.5) * 86400.0, timezone.utc)
assert abs((after - expect).total_seconds()) < 1e-3
seen = 0
for index in range(200):
case = fictional_case(index)
if int(case["year"]) >= 1970:
continue
zone = timezone(timedelta(hours=float(case["tz"])))
birth = datetime(
int(case["year"]), int(case["month"]), int(case["day"]), 12, 0, tzinfo=zone,
)
rows = hindu_solar_events(birth, float(case["lat"]), float(case["lon"]))
assert rows
seen += 1
assert seen > 0
def test_pyjhora_mismatch_labels_follow_the_committed_json() -> None:
"""Read the offline JSON. This test does not import jhora."""
path = ROOT / "docs" / "tasks" / "research-timing-pyjhora-20261023.json"
payload = json.loads(path.read_text(encoding="utf-8"))
assert payload["charts"] >= 5
assert payload["status"] == "compared"
systems = payload["systems"]
assert isinstance(systems, dict) and systems
case, _chart_data = _chart(0)
base = {"birth_info": case, "worksheets": {}, "calculation_profile": {}}
from scripts.pl9_full_data_export import render_full_data_markdown
chinese = render_full_data_markdown(dict(base))
english_packet = dict(base)
english_packet["report_language"] = "en"
english = render_full_data_markdown(english_packet)
if any(item.get("status") == "不一致" for item in systems.values()):
assert "与 PyJHora 不一致:" in chinese
assert "Differs from PyJHora:" in english
zh_rows = _sections(chinese)
en_rows = _sections(english)
for name, item in systems.items():
status = item["status"]
assert status in {"一致", "不一致", "无此方法"}, name
same_lord = re.search(r"native (\w+) vs PyJHora (\w+)", str(item.get("example") or ""))
if same_lord:
assert same_lord.group(1) != same_lord.group(2), (name, item.get("example"))
for rows, heading, sentence in (
(zh_rows, item["heading_zh"], f"与 PyJHora 不一致:差在 {item.get('category') or ''}"),
(en_rows, item["heading_en"], f"Differs from PyJHora: {item.get('category_en') or ''}"),
):
bodies = _under(rows, heading, item.get("match") or "exact")
assert bodies, (name, heading)
if status == "不一致":
assert all(sentence in body for body in bodies), (name, heading)
else:
assert all("与 PyJHora 不一致:" not in body and "Differs from PyJHora:" not in body for body in bodies), name
tree = ast.parse((ROOT / "scripts" / "research" / "research_timing_pyjhora_compare.py").read_text(encoding="utf-8"))
allowed = {"pyjhora_fingerprints", "_py_tithi_return_days"}
for node in tree.body:
if isinstance(node, ast.FunctionDef) and node.name in allowed:
continue
for child in ast.walk(node):
module = ""
if isinstance(child, ast.Import):
module = " ".join(alias.name for alias in child.names)
elif isinstance(child, ast.ImportFrom):
module = child.module or ""
assert not module.startswith("jhora"), module
def test_rectification_golden_file_is_unchanged() -> None:
digest = hashlib.sha256((ROOT / _GOLDEN).read_bytes()).hexdigest()
assert digest == "92efd8fdbae43f553b0006cfe28e929d5255fe02f3c36e616475fa149a356212"