fix(report): use the upstream reader edition for new longform bodies

New reports request reader_main from upstream origin/main 23b9609e.
KP and transit no longer copy an empty vars() dict, solar returns keep
birth_asc_sign_idx, and ordinary projection deletes internal lines whole.
This commit is contained in:
jesse-ux
2026-09-25 02:21:46 +08:00
parent 59c2301838
commit 3c2f7bd559
22 changed files with 3126 additions and 65 deletions
+32 -3
View File
@@ -226,12 +226,41 @@ def _first_available_field(label: str, *reports_and_key: Any) -> dict[str, Any]:
return {"status": "blocked", "producer": label, "reason": f"{key}_missing", "producers_checked": producers}
def _annual_comparable_value(key: str, value: Any) -> Any:
"""Compare sign and lord only. Extra dict keys are not a disagreement."""
if not isinstance(value, dict):
return value
if key == "muntha":
return {
"muntha_sign": value.get("muntha_sign"),
"muntha_lord": value.get("muntha_lord"),
}
if key == "year_lord":
return {
"year_lord": value.get("year_lord") or value.get("lord"),
"year_lord_sign": value.get("year_lord_sign") or value.get("sign"),
}
return {
"sign": value.get("sign") or value.get("muntha_sign") or value.get("year_lord_sign"),
"lord": value.get("lord") or value.get("muntha_lord") or value.get("year_lord"),
}
def _field_value_is_blocked(value: Any) -> bool:
return isinstance(value, dict) and value.get("status") == "blocked"
def _field_with_conflict_gate(key: str, solar_report: dict[str, Any], tajika_report: dict[str, Any]) -> dict[str, Any]:
values = []
for producer, report in (("solar_return", solar_report), ("tajika", tajika_report)):
if isinstance(report, dict) and report.get(key) is not None and not report.get("error"):
values.append({"producer": producer, "value": report[key]})
if len(values) >= 2 and values[0]["value"] != values[1]["value"]:
if not isinstance(report, dict) or report.get(key) is None or report.get("error"):
continue
value = report[key]
if _field_value_is_blocked(value):
continue
values.append({"producer": producer, "value": value})
comparable_values = [_annual_comparable_value(key, item["value"]) for item in values]
if len(values) >= 2 and comparable_values[0] != comparable_values[1]:
return {
"status": "conflict",
"field": key,
+35 -26
View File
@@ -1975,6 +1975,29 @@ def _personal_report_source_pack_contracts(
}
def _copy_call_namespace(args):
"""Copy CLI args without using an empty instance ``vars()`` dict.
``type('Args', (), fields)()`` stores fields on the class, so ``vars(args)``
is ``{}`` and ``SimpleNamespace(**vars(args))`` drops ``year`` and the rest.
argparse and SimpleNamespace keep fields on the instance. Upstream has the
same ``vars()`` copy.
"""
fields = {}
instance = getattr(args, "__dict__", None)
if isinstance(instance, dict):
fields.update({
key: value for key, value in instance.items() if not str(key).startswith("_")
})
cls = type(args)
if cls is not SimpleNamespace:
for key, value in vars(cls).items():
if str(key).startswith("_") or callable(value):
continue
fields.setdefault(key, value)
return SimpleNamespace(**fields)
def _attach_annual_tajika_pack(packet: dict, args) -> dict:
worksheets = packet.get('worksheets') if isinstance(packet.get('worksheets'), dict) else {}
timing = worksheets.get('timing_and_predictive_systems') if isinstance(worksheets.get('timing_and_predictive_systems'), dict) else None
@@ -1997,7 +2020,7 @@ def _attach_annual_tajika_pack(packet: dict, args) -> dict:
annual_years = {str(target_year): _annual_series_surface(timing['annual_tajika_pack'])}
for year in range(target_year + 1, target_year + 3):
annual_args = SimpleNamespace(**vars(args))
annual_args = _copy_call_namespace(args)
annual_args.target_year = year
try:
birth_year = int(getattr(args, 'year'))
@@ -2698,12 +2721,13 @@ def _native_dasha_master_families(modules: dict) -> dict:
def _blocked_planned_section(heading: str, reason_code: str, detail: str = "") -> list[str]:
"""Emit a planned heading with an explicit blocked reason instead of vanishing."""
lines = [heading, '', f"`blocked` / `{reason_code}`"]
if detail:
lines.append(str(detail).strip())
lines.append('')
return lines
"""Unavailable sections are omitted entirely, including raw error text.
Upstream returns an empty block when a producer has nothing to show.
Printing ``error`` / ``reason`` put traceback text into the reference body.
"""
del heading, reason_code, detail
return []
PLANNED_LONGFORM_SECTION_HEADINGS = (
@@ -3270,12 +3294,6 @@ def _render_finished_reading_navigation(packet: dict) -> list[str]:
'4. 校时敏感层:有候选窗才读矩阵,没有则视为未做校时。',
'5. 质量验收矩阵与对照覆盖表,只用来核对装配,不改写正文标签。',
'',
'### 结论等级规则',
'',
'- `executed` / `partial_verified`:可阅读,但仍受参数与证据边界约束。',
'- `parameter_sensitive`:可交叉核对,不得升级为精确事件。',
'- `blocked` / `missing_in_local`:保持原标签,不得涂成可读结论。',
'',
'### 专题判读协议',
'',
'- 事业先看 2/6/10/11 宫 KP 结构与 D10,再看三年月度支持。',
@@ -3605,17 +3623,13 @@ def render_pl9_markdown(packet: dict) -> str:
if not isinstance(notice, dict):
return []
primary_zh = notice.get('primary_text_zh') or notice.get('text_zh') or notice.get('summary_zh') or ''
primary_en = notice.get('primary_text_en') or notice.get('text_en') or ''
display_rule = notice.get('display_rule') or ''
if not any([primary_zh, primary_en, display_rule]):
if not any([primary_zh, display_rule]):
return []
lines_out = ['', '## 多引擎口径说明', '']
if primary_zh:
lines_out.append(str(primary_zh))
lines_out.append('')
if primary_en:
lines_out.append(str(primary_en))
lines_out.append('')
if display_rule:
lines_out.append(f"显示规则:{display_rule}")
lines_out.append('')
@@ -13609,7 +13623,7 @@ def cmd_ashtakoot(args):
# ============================================================================
def cmd_kp(args):
requested_report_ayanamsa = _current_ayanamsa_name(args)
kp_args = SimpleNamespace(**vars(args))
kp_args = _copy_call_namespace(args)
kp_args.ayanamsa = 'kp'
chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(kp_args)
@@ -15907,7 +15921,7 @@ def cmd_full_reading(args):
# They are intentionally not fed into conclusion generation or timing promotion.
reference_date = getattr(args, 'transit_date', None) or getattr(args, 'today', None)
if reference_date:
transit_args = SimpleNamespace(**vars(args))
transit_args = _copy_call_namespace(args)
transit_args.date = reference_date
transit_args.transit_date = reference_date
transit_args.house = 7
@@ -17735,12 +17749,7 @@ def _professional_report_reference_boundary_notice() -> dict:
'不同引擎的岁差、节点、宫制、分盘和大运变体可能产生差异;'
'冲突、参数敏感或未验证项目会保留边界标签,不合并成单一确定结论。'
),
'primary_text_en': (
'This reference export combines local Jyotish calculations with available external-engine '
'replays and source comparisons. Engine-specific settings can differ, so conflicts and '
'unverified variants remain explicitly labeled rather than being collapsed into one claim.'
),
'display_rule': '以本报告标注的 executed / blocked / parameter_sensitive 状态为准。',
'display_rule': '冲突和未验证的项目保留边界,不合并成单一确定结论。',
}
File diff suppressed because it is too large Load Diff
+34 -1
View File
@@ -26,6 +26,24 @@ class ProfessionalReportReferenceInputError(ValueError):
"""The professional-reference request is outside the public contract."""
READER_MAIN_EDITION = "reader_main"
REFERENCE_EDITION = "reference"
REPORT_VERSION_READER = "pl9_personal_long_report.v3"
def _normalize_edition(value: Any) -> str:
if value is None or value == "":
return REFERENCE_EDITION
if not isinstance(value, str):
raise ProfessionalReportReferenceInputError("edition must be a string")
normalized = value.strip()
if normalized in {"", REFERENCE_EDITION, "professional_reference"}:
return REFERENCE_EDITION
if normalized == READER_MAIN_EDITION:
return READER_MAIN_EDITION
raise ProfessionalReportReferenceInputError("edition must be reference or reader_main")
def _normalize_format(value: Any) -> str:
if value is None:
return "json"
@@ -100,6 +118,7 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine
if not isinstance(body, dict):
raise ProfessionalReportReferenceInputError("request body must be an object")
output_format = _normalize_format(body.get("format"))
edition = _normalize_edition(body.get("edition"))
packs = _normalize_packs(body.get("packs"))
birth = handler._high_rigor_birth_payload(body)
@@ -114,15 +133,29 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine
except ValueError as exc:
raise ProfessionalReportReferenceInputError(str(exc)) from exc
if edition == READER_MAIN_EDITION and isinstance(packet, dict):
packet = dict(packet)
packet["report_version"] = REPORT_VERSION_READER
if output_format == "markdown":
if edition == READER_MAIN_EDITION:
try:
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path
from pl9_reader_export import _pl9_export_markdown_for_edition
markdown = _pl9_export_markdown_for_edition(packet, READER_MAIN_EDITION)
else:
markdown = resolved_engine.render_pl9_markdown(packet)
return {
"format": "markdown",
"markdown": resolved_engine.render_pl9_markdown(packet),
"edition": edition,
"report_version": REPORT_VERSION_READER if edition == READER_MAIN_EDITION else packet.get("report_version"),
"markdown": markdown,
**({"fact_table_packet": report_density_packet(packet),
"reader_dasha_applicability": clean_reader_appendix_markdown(render_dasha_applicability(packet))}
if body.get("include_fact_tables") is True else {}),
}
return {
"format": "json",
"edition": edition,
"report": packet,
}
+1 -1
View File
@@ -15,7 +15,7 @@ from typing import Any
AUTHORITY_SCHEMA_VERSION = "jyotish.shared_full_report_authority.v1"
REPORT_SCHEMA_VERSION = "expert_report_output.v2"
DEFAULT_REPORT_VERSION = "pl9_personal_long_report.v2"
DEFAULT_REPORT_VERSION = "pl9_personal_long_report.v3"
PL9_EXPORT_SCHEMA = "pl9_style_professional_export_v1"
QUALITY_GATE_SCHEMA = "jyotish.full_report_quality_gate.v1"
+15 -8
View File
@@ -401,6 +401,7 @@ def calc_solar_return_chart(
'houses': houses,
'ascendant': ascendant,
'asc_sign_idx': sr_asc_idx,
'birth_asc_sign_idx': birth_asc_idx,
'target_year': target_year,
'birth_year': birth_year,
'age': target_year - birth_year,
@@ -505,14 +506,20 @@ def solar_return_full_report(
# 3. Year Lord(= Muntha 守护星)
try:
muntha_data = result.get('muntha', {})
muntha_sign_val = muntha_data.get('muntha_sign_idx', muntha_sign if 'muntha_sign' in dir() else 0)
year_lord_sign = muntha_sign_val
year_lord = SIGN_LORDS[SIGNS[year_lord_sign]]
result['year_lord'] = {
'year_lord': year_lord,
'year_lord_sign_idx': year_lord_sign,
'year_lord_sign': SIGNS[year_lord_sign],
}
muntha_sign_val = muntha_data.get('muntha_sign_idx') if isinstance(muntha_data, dict) else None
if not isinstance(muntha_sign_val, int) or isinstance(muntha_sign_val, bool):
result['year_lord'] = {
'status': 'blocked',
'reason': 'muntha_sign_required_for_year_lord',
}
else:
year_lord_sign = muntha_sign_val % 12
year_lord = SIGN_LORDS[SIGNS[year_lord_sign]]
result['year_lord'] = {
'year_lord': year_lord,
'year_lord_sign_idx': year_lord_sign,
'year_lord_sign': SIGNS[year_lord_sign],
}
except Exception as e:
result['year_lord'] = {'error': str(e)}