引擎在同一份 packet 上给出与对话同源卡相同的事实(include_reading_facts), 报告任务交付前用一次结构化模型调用写八节解读,校验不过或失败就不出解读、 报告照常交付。解读存在计算快照里,读接口一起返回,页面放在引擎正文之前, 后面标「计算依据」。用量按实际 token 记账(BUG-1460、BUG-1461)。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3
168 lines
7.4 KiB
Python
168 lines
7.4 KiB
Python
"""Reading facts for the model-written report reading (issue #113 R1).
|
|
|
|
Clearly fictional chart only. The facts must be copies of the report's own
|
|
packet (no new algorithm), the flag must default off with the response
|
|
unchanged, and the flag must be part of the report cache key.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
|
|
import pytest
|
|
|
|
import scripts.professional_report_reference as reference
|
|
from scripts.consult_card_domain_facts import dig
|
|
from scripts.jyotish_engine import build_professional_report_reference_packet, cmd_full_reading
|
|
from scripts.professional_report_reference import _export_args, build_professional_report_reference
|
|
from scripts.report_render.reading_facts import READING_DOMAINS, build_report_reading_facts
|
|
from tests.test_report_reader_main import FICTIONAL
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def chart():
|
|
case = dict(FICTIONAL)
|
|
reading = cmd_full_reading(type("Args", (), case)())
|
|
packet = dict(build_professional_report_reference_packet(reading, _export_args(case), []))
|
|
packet["report_version"] = "pl9_personal_long_report.v3"
|
|
return case, reading, packet
|
|
|
|
|
|
def test_reading_facts_are_packet_leaves(chart) -> None:
|
|
_case, reading, packet = chart
|
|
before = json.dumps(packet, ensure_ascii=False, sort_keys=True, default=str)
|
|
facts = build_report_reading_facts(packet, reading, minute_confirmed=True, today="2026-04-07")
|
|
# Building the facts never touches the report's packet.
|
|
assert json.dumps(packet, ensure_ascii=False, sort_keys=True, default=str) == before
|
|
assert facts["schema"] == "report-reading-facts-v1"
|
|
assert sorted(facts["domains"]) == sorted(READING_DOMAINS)
|
|
for name, planet in facts["planets"].items():
|
|
assert planet["d1_sign"] == dig(packet, f"core_chart.planets.{name}.sign")
|
|
assert planet["d1_house"] == dig(packet, f"core_chart.planets.{name}.house")
|
|
assert planet["d1_dignity"] == dig(packet, f"core_chart.planets.{name}.status")
|
|
for key in ("sign", "lord", "nakshatra"):
|
|
assert facts["ascendant"][key] == dig(packet, f"core_chart.ascendant.{key}")
|
|
current = dig(packet, "worksheets.timing_and_predictive_systems.dasha.current_dasha")
|
|
assert facts["timing"]["mahadasha"] == {"lord": current["lord"], "start": current["start"], "end": current["end"]}
|
|
rows = current["antardasha_timeline"]
|
|
index = next(i for i, row in enumerate(rows) if row.get("is_current") is True)
|
|
assert facts["timing"]["antardasha"]["lord"] == rows[index]["lord"]
|
|
assert [row["lord"] for row in facts["timing"]["next_antardashas"]] == [row["lord"] for row in rows[index + 1:index + 3]]
|
|
# Paths, checks and gaps stay out of what the model reads.
|
|
encoded = json.dumps(facts, ensure_ascii=False)
|
|
for marker in ('"path"', '"check"', '"status"', '"citation"', "references/"):
|
|
assert marker not in encoded
|
|
assert len(encoded) < 20_000
|
|
|
|
|
|
def test_withheld_varga_houses_stay_out_without_a_confirmed_minute(chart) -> None:
|
|
_case, reading, packet = chart
|
|
facts = build_report_reading_facts(packet, reading, minute_confirmed=False)
|
|
assert facts["minute_confirmed"] is False
|
|
assert "出生时间精度不够" not in json.dumps(facts, ensure_ascii=False)
|
|
|
|
|
|
class _Handler:
|
|
def __init__(self, case: dict, reading: dict) -> None:
|
|
self.case = case
|
|
self.reading = reading
|
|
self.calls = 0
|
|
|
|
def _high_rigor_birth_payload(self, body: dict) -> dict:
|
|
payload = {key: value for key, value in self.case.items() if key != "name"}
|
|
if body.get("today"):
|
|
payload["today"] = body["today"]
|
|
return payload
|
|
|
|
def _compute_full_reading_for_thematic(self, _birth: dict) -> dict:
|
|
self.calls += 1
|
|
return self.reading
|
|
|
|
|
|
@pytest.fixture
|
|
def stubbed(chart, monkeypatch, tmp_path):
|
|
import scripts.consult_full_data_cache as cache
|
|
|
|
case, reading, packet = chart
|
|
|
|
def _dir(name: str = "consult_full_data_cache"):
|
|
path = tmp_path / name
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
class _Engine:
|
|
def build_professional_report_reference_packet(self, _reading, _args, _packs, **_kwargs):
|
|
return copy.deepcopy(packet)
|
|
|
|
def render_pl9_markdown(self, _packet):
|
|
return "reference"
|
|
|
|
monkeypatch.setattr(cache, "cache_dir", _dir)
|
|
monkeypatch.setattr(reference, "render_full_data_markdown", lambda p: "en-body" if p.get("report_language") == "en" else "zh-body")
|
|
monkeypatch.setattr(reference, "report_density_packet", lambda _packet: {"facts": 1})
|
|
monkeypatch.setattr(reference, "render_dasha_applicability", lambda _packet, language="zh": f"app-{language}")
|
|
monkeypatch.setattr(reference, "clean_reader_appendix_markdown", lambda text: text)
|
|
monkeypatch.setenv("JYOTISH_REPORT_RENDER_FAST", "0")
|
|
monkeypatch.setenv("JYOTISH_REPORT_REFERENCE_CACHE", "0")
|
|
return _Handler(case, reading), _Engine()
|
|
|
|
|
|
def _body(**extra) -> dict:
|
|
body = {"format": "markdown", "edition": "full_data", "include_fact_tables": True, "languages": ["zh"], "today": "2026-04-07"}
|
|
body.update(extra)
|
|
return body
|
|
|
|
|
|
def _dumps(result: dict) -> str:
|
|
return json.dumps(result, ensure_ascii=False, sort_keys=True, default=str)
|
|
|
|
|
|
def test_flag_off_response_is_unchanged(stubbed) -> None:
|
|
handler, engine = stubbed
|
|
absent = build_professional_report_reference(handler, _body(), engine=engine)
|
|
off = build_professional_report_reference(handler, _body(include_reading_facts=False), engine=engine)
|
|
assert _dumps(absent) == _dumps(off)
|
|
assert "reading_facts" not in absent
|
|
assert sorted(absent) == ["edition", "fact_table_packet", "format", "markdown", "reader_dasha_applicability", "report_version"]
|
|
|
|
|
|
def test_flag_on_adds_reading_facts_and_keeps_the_report(stubbed) -> None:
|
|
handler, engine = stubbed
|
|
plain = build_professional_report_reference(handler, _body(), engine=engine)
|
|
with_facts = build_professional_report_reference(
|
|
handler, _body(include_reading_facts=True, birth_time_accuracy="confirmed", hour=9, minute=15), engine=engine,
|
|
)
|
|
facts = with_facts.pop("reading_facts")
|
|
assert _dumps(with_facts) == _dumps(plain)
|
|
assert facts["schema"] == "report-reading-facts-v1"
|
|
assert facts["minute_confirmed"] is True
|
|
assert facts["today"] == "2026-04-07"
|
|
assert "gaps" not in facts
|
|
|
|
|
|
def test_reading_facts_failure_never_fails_the_report(stubbed, monkeypatch) -> None:
|
|
import scripts.report_render.reading_facts as module
|
|
|
|
handler, engine = stubbed
|
|
|
|
def broken(*_args, **_kwargs):
|
|
raise RuntimeError("boom")
|
|
|
|
monkeypatch.setattr(module, "build_report_reading_facts", broken)
|
|
result = build_professional_report_reference(handler, _body(include_reading_facts=True), engine=engine)
|
|
assert result["markdown"] == "zh-body"
|
|
assert result["reading_facts"]["gaps"] == ["reading_facts_unavailable"]
|
|
|
|
|
|
def test_reading_flag_is_part_of_the_report_cache_key(stubbed, monkeypatch) -> None:
|
|
handler, engine = stubbed
|
|
monkeypatch.setenv("JYOTISH_REPORT_REFERENCE_CACHE", "1")
|
|
monkeypatch.setenv("GITHUB_SHA", "reading-cache-test")
|
|
build_professional_report_reference(handler, _body(), engine=engine)
|
|
build_professional_report_reference(handler, _body(include_reading_facts=True), engine=engine)
|
|
assert handler.calls == 2
|
|
cached = build_professional_report_reference(handler, _body(include_reading_facts=True), engine=engine)
|
|
assert handler.calls == 2
|
|
assert "reading_facts" in cached
|