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:
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user