feat(reports): add professional reference export
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
@@ -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='显式指定可复现执行归档目录;不指定时不落盘归档')
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user