feat(reports): add professional reference export

This commit is contained in:
Jesse_Chen
2026-09-04 00:18:29 +08:00
parent c2f23131f7
commit 3b09bbbee0
11 changed files with 715 additions and 48 deletions
+1
View File
@@ -30,6 +30,7 @@ HEAVY_COMPUTE_PATHS = frozenset(
"/api/high_rigor_workflow",
"/api/consultation_workflow",
"/api/professional_reading",
"/api/professional_report_reference",
"/api/vedastro/range_scan",
"/api/vedastro_gateway/run",
"/api/thematic_report",
+7
View File
@@ -3515,6 +3515,13 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
elif path == '/api/professional_reading':
result = self._compute_professional_reading(body)
self._json(result)
elif path == '/api/professional_report_reference':
reference_module = _load_local_module('professional_report_reference')
try:
result = reference_module.build_professional_report_reference(self, body)
except reference_module.ProfessionalReportReferenceInputError as exc:
raise BadRequest(str(exc)) from exc
self._json(result)
elif path == '/api/import_chart':
result = self._import_chart_text(body)
self._json(result)
+157 -45
View File
@@ -9592,7 +9592,10 @@ def render_pl9_markdown(packet: dict) -> str:
lines.extend(['', render_pl9_continuation_prompt()])
return '\n'.join(lines).strip() + '\n'
markdown = '\n'.join(lines).strip() + '\n'
if isinstance(packet.get('reader_engine_boundary_notice'), dict):
return sanitize_professional_report_reference_markdown(markdown)
return markdown
def write_pl9_pdf(packet: dict, output_path: str | None = None) -> dict:
@@ -16906,54 +16909,163 @@ def _attach_default_chart_identity(packet: dict) -> dict:
return packet
_PROFESSIONAL_REPORT_REFERENCE_OMITTED_KEYS = frozenset({
'raw_full_reading',
'fixture_path',
'source_path',
'artifact_path',
'module_path',
'source_crop',
'source_file',
'source_module',
'internal_module',
'implementation_path',
})
_PROFESSIONAL_REPORT_REFERENCE_INTERNAL_VALUE = re.compile(
r'(?:'
r'(?:^|[\\/])(?:Users|home|private|tmp)[\\/]'
r'|[A-Za-z]:\\\\'
r'|(?:^|[\\s`\"\'])scripts?[\\/]'
r'|\.py(?:::|\b)'
r'|\braw_full_reading\b'
r'|\bjyotish_engine\.'
r'|\b[a-zA-Z_][a-zA-Z0-9_]*_(?:engine|service|adapter|contract|producer)\.[A-Za-z_]'
r')'
)
_PROFESSIONAL_REPORT_REFERENCE_INTERNAL_TEXT = re.compile(
r'(?:'
r'\braw_full_reading(?:\.[A-Za-z0-9_]+)*'
r'|\bscripts?[\\/][A-Za-z0-9_.\\/-]+'
r'|\b[A-Za-z_][A-Za-z0-9_]*\.py::[A-Za-z_][A-Za-z0-9_.]*'
r'|\bjyotish_engine\.[A-Za-z_][A-Za-z0-9_.]*'
r'|\b[a-zA-Z_][a-zA-Z0-9_]*_(?:engine|service|adapter|contract|producer)\.[A-Za-z_][A-Za-z0-9_.]*'
r'|/(?:Users|home|private|tmp)/[^\s`\"\'|)\]]+'
r'|[A-Za-z]:\\[^\s`\"\'|)\]]+'
r')'
)
def sanitize_professional_report_reference_markdown(markdown: str) -> str:
"""Redact renderer-authored implementation references from public Markdown."""
return _PROFESSIONAL_REPORT_REFERENCE_INTERNAL_TEXT.sub(
'internal_reference_omitted',
str(markdown),
)
def _professional_report_reference_boundary_notice() -> dict:
return {
'schema': 'jyotish.reader_engine_boundary_notice.v1',
'status': 'declared',
'primary_text_zh': (
'本参考版汇集本地 Jyotish 计算、已接入的外部引擎回放与资料对照。'
'不同引擎的岁差、节点、宫制、分盘和大运变体可能产生差异;'
'冲突、参数敏感或未验证项目会保留边界标签,不合并成单一确定结论。'
),
'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 状态为准。',
}
def sanitize_professional_report_reference(value, *, field_name=None):
"""Remove runtime implementation details from the public reference export."""
normalized_field = str(field_name or '').strip().lower()
if normalized_field in _PROFESSIONAL_REPORT_REFERENCE_OMITTED_KEYS or normalized_field.endswith('_path'):
return None
if value is None or isinstance(value, (int, float, bool)):
return value
if isinstance(value, str):
if _PROFESSIONAL_REPORT_REFERENCE_INTERNAL_VALUE.search(value):
return 'internal_reference_omitted'
return value
if isinstance(value, dict):
sanitized = {}
for key, item in value.items():
key_text = str(key)
normalized_key = key_text.strip().lower()
if normalized_key in _PROFESSIONAL_REPORT_REFERENCE_OMITTED_KEYS or normalized_key.endswith('_path'):
continue
sanitized_item = sanitize_professional_report_reference(item, field_name=key_text)
if sanitized_item is not None:
sanitized[key_text] = sanitized_item
return sanitized
if isinstance(value, (list, tuple, set)):
return [
sanitized
for item in value
if (sanitized := sanitize_professional_report_reference(item)) is not None
]
rendered = str(value)
if _PROFESSIONAL_REPORT_REFERENCE_INTERNAL_VALUE.search(rendered):
return 'internal_reference_omitted'
return rendered
def build_professional_report_reference_packet(
full_reading: dict,
args,
selected_pack_ids: list[str] | None = None,
) -> dict:
"""Build the public, non-writer professional reference packet for CLI and API."""
packet = build_pl9_style_export_packet(full_reading, include_raw=False)
packet = attach_calculation_profile(packet, args)
packet = _attach_annual_tajika_pack(packet, args)
packet = _attach_report_governance_contracts(packet, args)
packet = _attach_base_charts_pack(packet)
packet = _attach_visual_chart_pack(packet, args)
selected = _apply_pl9_pack_selection(packet, selected_pack_ids or [])
final_packet = _attach_full_report_pack(selected, args)
final_packet = _attach_startrack_language_bridge(final_packet, args)
final_packet = attach_calculation_profile(final_packet, args)
final_packet = _attach_default_chart_identity(final_packet)
final_packet = _attach_report_governance_contracts(final_packet, args)
final_packet['reader_engine_boundary_notice'] = _professional_report_reference_boundary_notice()
final_packet['generated_at'] = datetime.utcnow().replace(microsecond=0).isoformat() + 'Z'
if final_packet.get('selected_report_scope') == 'full':
evaluate_full_report = _try_attr_import('full_report_quality_gate', 'evaluate_full_report')
if evaluate_full_report is None:
final_packet['report_quality_gate'] = {
'status': 'blocked',
'reason': 'full_report_quality_gate_absent',
}
else:
public_quality_input = sanitize_professional_report_reference(final_packet)
final_packet['report_quality_gate'] = evaluate_full_report(
public_quality_input,
render_pl9_markdown(public_quality_input),
)
build_shared_full_report_authority = _try_attr_import(
'shared_full_report_authority', 'build_shared_full_report_authority'
)
if build_shared_full_report_authority is None:
final_packet['shared_full_report_authority'] = {
'schema_version': 'jyotish.shared_full_report_authority.v1',
'status': 'blocked',
'reason': 'shared_full_report_authority_absent',
'read_only': True,
}
else:
final_packet['shared_full_report_authority'] = build_shared_full_report_authority(final_packet)
return sanitize_professional_report_reference(final_packet)
def cmd_pl9_export(args):
full = cmd_full_reading(args)
# PL9 is the research/authority export surface: never drop the raw reading
# behind an opt-in flag. The legacy flag remains accepted for CLI compatibility.
packet = build_pl9_style_export_packet(full, include_raw=True)
try:
packet = attach_calculation_profile(packet, args)
packet = _attach_annual_tajika_pack(packet, args)
packet = _attach_report_governance_contracts(packet, args)
packet = _attach_base_charts_pack(packet)
packet = _attach_visual_chart_pack(packet, args)
selected = _apply_pl9_pack_selection(packet, _normalize_pl9_pack_selection(args))
final_packet = _attach_full_report_pack(selected, args)
final_packet = _attach_startrack_language_bridge(final_packet, args)
final_packet = attach_calculation_profile(final_packet, args)
final_packet = _attach_default_chart_identity(final_packet)
final_packet = _attach_report_governance_contracts(final_packet, args)
# Added after result hashing so delivery provenance cannot alter chart computation.
final_packet['generated_at'] = datetime.utcnow().replace(microsecond=0).isoformat() + 'Z'
if final_packet.get('selected_report_scope') == 'full':
evaluate_full_report = _try_attr_import("full_report_quality_gate", "evaluate_full_report")
if evaluate_full_report is None:
final_packet['report_quality_gate'] = {
'status': 'blocked',
'reason': 'full_report_quality_gate_absent',
}
else:
reader_markdown_for_quality = render_pl9_markdown(final_packet)
final_packet['report_quality_gate'] = evaluate_full_report(
final_packet,
reader_markdown_for_quality,
)
build_shared_full_report_authority = _try_attr_import(
"shared_full_report_authority", "build_shared_full_report_authority"
)
if build_shared_full_report_authority is None:
final_packet['shared_full_report_authority'] = {
'schema_version': 'jyotish.shared_full_report_authority.v1',
'status': 'blocked',
'reason': 'shared_full_report_authority_absent',
'read_only': True,
}
else:
final_packet['shared_full_report_authority'] = build_shared_full_report_authority(final_packet)
return _build_response_envelope('pl9-export', final_packet, args=args, execution_status='executed')
return build_professional_report_reference_packet(
full,
args,
_normalize_pl9_pack_selection(args),
)
except ValueError as exc:
return {
'scope': 'pl9_export_pack_selection',
'scope': 'professional_report_reference_pack_selection',
'status': 'blocked',
'reason': str(exc),
}
@@ -17273,7 +17385,7 @@ def main():
p.add_argument('--transit-date', default=None, help='Transit真实过境参考日期 YYYY-MM-DD(默认跟随--today或今天)')
p.add_argument('--target-year', type=int, default=None, help='太阳返照盘目标年份(默认不计算 Varshaphala')
p.add_argument('--profile-stages', action='store_true', help='输出 full-reading 粗粒度阶段耗时,并在 summary 中附带 stage timings')
p.add_argument('--include-raw', action='store_true', help='兼容参数:PL9 导出始终附带原始 full-reading 完整输出')
p.add_argument('--include-raw', action='store_true', help='兼容参数:专业参考版始终省略 raw full-reading 与内部实现信息')
p.add_argument('--format', choices=['json', 'markdown', 'pdf', 'authority'], default='json', help='导出格式:json / markdown / pdf / authority')
p.add_argument('--output', default=None, help='pdf 导出目标路径(仅 --format pdf 生效)')
p.add_argument('--archive-dir', default=None, help='显式指定可复现执行归档目录;不指定时不落盘归档')
+102
View File
@@ -0,0 +1,102 @@
"""Public professional-reference export assembly for the Jyotish API.
This boundary performs no writing-agent, persistence, billing, or telemetry work.
It validates the public export request, reuses the handler's one full-reading
calculation, and delegates packet assembly/rendering to ``jyotish_engine``.
"""
from __future__ import annotations
from importlib import import_module
from types import SimpleNamespace
from typing import Any
class ProfessionalReportReferenceInputError(ValueError):
"""The professional-reference request is outside the public contract."""
def _normalize_format(value: Any) -> str:
if value is None:
return "json"
if not isinstance(value, str):
raise ProfessionalReportReferenceInputError("format must be json or markdown")
normalized = value.strip().lower()
if normalized not in {"json", "markdown"}:
raise ProfessionalReportReferenceInputError("format must be json or markdown")
return normalized
def _normalize_packs(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, str):
raw_items = value.split(",")
elif isinstance(value, list):
if any(not isinstance(item, str) for item in value):
raise ProfessionalReportReferenceInputError("packs must contain only strings")
raw_items = value
else:
raise ProfessionalReportReferenceInputError("packs must be a string or array of strings")
selected: list[str] = []
seen: set[str] = set()
for item in raw_items:
pack_id = item.strip()
if pack_id and pack_id not in seen:
selected.append(pack_id)
seen.add(pack_id)
return selected
def _load_engine():
try:
return import_module("scripts.jyotish_engine")
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path
return import_module("jyotish_engine")
def _export_args(birth: dict[str, Any]) -> SimpleNamespace:
normalized_birth = {
**birth,
"hour": int(birth["hour"]),
"minute": int(birth["minute"]),
"second": int(birth.get("second", 0)),
}
return SimpleNamespace(
**normalized_birth,
age=None,
target_year=None,
visual_chart_observations=None,
startrack_language_bridge=False,
)
def build_professional_report_reference(handler, body: dict[str, Any], *, engine=None) -> dict[str, Any]:
"""Build one JSON or Markdown response from one reused full-reading result."""
if not isinstance(body, dict):
raise ProfessionalReportReferenceInputError("request body must be an object")
output_format = _normalize_format(body.get("format"))
packs = _normalize_packs(body.get("packs"))
birth = handler._high_rigor_birth_payload(body)
full_reading = handler._compute_full_reading_for_thematic(birth)
resolved_engine = engine or _load_engine()
try:
packet = resolved_engine.build_professional_report_reference_packet(
full_reading,
_export_args(birth),
packs,
)
except ValueError as exc:
raise ProfessionalReportReferenceInputError(str(exc)) from exc
if output_format == "markdown":
return {
"format": "markdown",
"markdown": resolved_engine.render_pl9_markdown(packet),
}
return {
"format": "json",
"report": packet,
}