Files
Jyotisha/scripts/report_pack_contract.py
T
Jesse_Chen f224146348
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
feat(upstream): merge a6f47abd engine, MCP, and orchestrator
Three-way-merge calculation modules and pl9-export into the product fork while keeping commercial API routes, Raman ayanamsa, and the consultation contract as a keypath superset.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 20:28:57 +08:00

255 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Shared report-pack contract normalizer.
This module intentionally stays thin: it does not compute astrology results and
does not adjudicate pack truth. It converts existing pack-shaped dictionaries
into one stable envelope that the final PL9-style renderer can consume.
"""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
try:
from canonical_jyotish_profile import (
build_canonical_jyotish_profile,
build_disputed_method_policy,
build_reader_engine_boundary_notice,
)
except Exception: # pragma: no cover - research helpers are not vendored here
try:
from scripts.canonical_jyotish_profile import (
build_canonical_jyotish_profile,
build_disputed_method_policy,
build_reader_engine_boundary_notice,
)
except Exception:
def build_canonical_jyotish_profile():
return {"profile_id": "product_canonical", "status": "local_fallback"}
def build_disputed_method_policy():
return {}
def build_reader_engine_boundary_notice():
return {"status": "local_fallback"}
try:
from profile_aware_benchmark_boundary_dashboard import load_dashboard_payload
except Exception: # pragma: no cover - research helpers are not vendored here
try:
from scripts.profile_aware_benchmark_boundary_dashboard import load_dashboard_payload
except Exception:
def load_dashboard_payload():
return {"status": "blocked", "reason": "profile_aware_dashboard_absent"}
SCHEMA = "pl9.unified_report_pack_contract.v1"
def normalize_report_pack_contract(pack: Mapping[str, Any], *, pack_id: str) -> dict[str, Any]:
"""Return a uniform report-ready envelope for an existing pack."""
report_sections = _normalize_report_sections(pack.get("report_sections"))
audit = _normalize_audit(pack)
pl9_pages = _as_list(pack.get("pl9_pages") or audit.get("pl9_pages"))
blocked_reasons = _blocked_reasons(pack, report_sections)
contract = {
"schema": SCHEMA,
"pack_id": pack_id,
"source_schema": pack.get("schema"),
"status": _status(pack, audit),
"profile": dict(pack.get("profile") or {}),
"pl9_pages": pl9_pages,
"summary": dict(pack.get("summary") or {}),
"canonical_jyotish_profile": dict(pack.get("canonical_jyotish_profile") or build_canonical_jyotish_profile()),
"reader_engine_boundary_notice": dict(
pack.get("reader_engine_boundary_notice") or build_reader_engine_boundary_notice()
),
"multi_engine_difference_notice": _multi_engine_difference_notice(pack),
"profile_aware_dashboard": dict(pack.get("profile_aware_dashboard") or load_dashboard_payload()),
"raw_data": _raw_data(pack),
"normalized_data": _normalized_data(pack),
"report_sections": report_sections,
"exports": _normalize_exports(pack.get("exports")),
"audit": audit,
"blocked_reasons": blocked_reasons,
"contract_audit": {
"missing_fields": _missing_fields(report_sections),
"source_pack_keys": list(pack.keys()),
"normalization_boundary": "contract_only_no_astrological_recalculation",
},
}
return contract
def _status(pack: Mapping[str, Any], audit: Mapping[str, Any]) -> str:
explicit = pack.get("status") or audit.get("status")
if explicit:
return str(explicit)
statuses: list[str] = []
report_sections = pack.get("report_sections") if isinstance(pack.get("report_sections"), Mapping) else {}
for key in ("executive_summary", "thematic_narrative", "evidence_appendix", "pdf_sections"):
section = report_sections.get(key)
if isinstance(section, Mapping) and section.get("status"):
statuses.append(str(section.get("status")))
for key, value in pack.items():
if key in {"schema", "profile", "pl9_pages", "summary", "report_sections", "exports", "audit"}:
continue
if isinstance(value, Mapping) and value.get("status"):
statuses.append(str(value.get("status")))
if not statuses:
return "blocked"
if any(status in {"conflict", "parameter_sensitive"} for status in statuses):
return "parameter_sensitive"
if any(status in {"partial_verified", "verified"} for status in statuses):
return "partial_verified"
if any(status == "not_applicable" for status in statuses):
return "not_applicable"
return "blocked"
def _normalize_report_sections(value: Any) -> dict[str, Any]:
sections = dict(value or {}) if isinstance(value, Mapping) else {}
executive = sections.get("executive_summary")
thematic = sections.get("thematic_narrative")
evidence = sections.get("evidence_appendix")
pdf_sections = sections.get("pdf_sections")
if isinstance(executive, Mapping):
executive = [str(executive.get("headline") or executive.get("status") or "")]
elif isinstance(executive, str):
executive = [executive]
elif executive is None:
executive = []
else:
executive = list(executive)
if thematic is None:
thematic = sections.get("trigger_seed_narrative") or []
if evidence is None:
evidence = []
for key in ("visual_chart_audit", "audit_appendix"):
if key in sections:
evidence.append(sections[key])
return {
"executive_summary": executive,
"thematic_narrative": list(thematic) if isinstance(thematic, list) else _as_list(thematic),
"evidence_appendix": list(evidence) if isinstance(evidence, list) else _as_list(evidence),
"pdf_sections": list(pdf_sections) if isinstance(pdf_sections, list) else _as_list(pdf_sections),
}
def _normalize_exports(value: Any) -> dict[str, Any]:
exports = dict(value or {}) if isinstance(value, Mapping) else {}
return {
"json": exports.get("json"),
"markdown": exports.get("markdown"),
"pdf_sections": exports.get("pdf_sections"),
"ai_evidence_bundle": dict(exports.get("ai_evidence_bundle") or {}),
}
def _multi_engine_difference_notice(pack: Mapping[str, Any]) -> dict[str, Any]:
if isinstance(pack.get("multi_engine_difference_notice"), Mapping):
return dict(pack["multi_engine_difference_notice"])
policy = build_disputed_method_policy()
lanes = []
for lane_id, lane in policy.items():
lanes.append(
{
"lane_id": lane_id,
"canonical_standard": lane["canonical_standard"],
"external_difference_display": "external_observation_conflict",
"report_assertion_ceiling": lane["report_assertion_ceiling"],
"reader_text_zh": (
f"{lane_id} 主口径按 {lane['canonical_standard']}"
"其他引擎若不同,会显示为外部观察冲突,不直接改写主结论。"
),
}
)
return {
"status": "active",
"purpose": "reader_safe_multi_engine_difference_display",
"canonical_profile_id": build_canonical_jyotish_profile()["profile_id"],
"lanes": lanes,
"must_not_claim": [
"external_engine_conflict_overrides_canonical_result",
"internal_consistency_is_global_truth_closure",
],
}
def _normalize_audit(pack: Mapping[str, Any]) -> dict[str, Any]:
audit = dict(pack.get("audit") or {})
must_not_claim = list(audit.get("must_not_claim") or [])
sections = pack.get("report_sections") if isinstance(pack.get("report_sections"), Mapping) else {}
for key in ("visual_chart_audit", "evidence_audit"):
section = sections.get(key) if isinstance(sections, Mapping) else None
if isinstance(section, Mapping):
must_not_claim.extend(item for item in section.get("must_not_claim") or [] if item not in must_not_claim)
if must_not_claim:
audit["must_not_claim"] = must_not_claim
return audit
def _blocked_reasons(pack: Mapping[str, Any], report_sections: Mapping[str, Any]) -> list[str]:
reasons: list[str] = []
for key in ("blocked_reasons", "blocked_fields"):
reasons.extend(str(item) for item in pack.get(key) or [])
original_sections = pack.get("report_sections") if isinstance(pack.get("report_sections"), Mapping) else {}
executive = original_sections.get("executive_summary") if isinstance(original_sections, Mapping) else None
if isinstance(executive, Mapping):
reasons.extend(str(item) for item in executive.get("blocked_fields") or [])
if not reasons and _status(pack, pack.get("audit") or {}) == "blocked":
for item in report_sections.get("evidence_appendix") or []:
if isinstance(item, str):
reasons.append(item)
return list(dict.fromkeys(reasons))
def _raw_data(pack: Mapping[str, Any]) -> dict[str, Any]:
return {
key: value
for key, value in pack.items()
if key
not in {
"schema",
"status",
"profile",
"pl9_pages",
"summary",
"report_sections",
"exports",
"audit",
"profile_aware_dashboard",
}
}
def _normalized_data(pack: Mapping[str, Any]) -> dict[str, Any]:
return {
"source_schema": pack.get("schema"),
"status": pack.get("status") or (pack.get("audit") or {}).get("status") if isinstance(pack.get("audit"), Mapping) else pack.get("status"),
}
def _missing_fields(report_sections: Mapping[str, Any]) -> list[str]:
missing = []
for key in ("executive_summary", "thematic_narrative", "evidence_appendix"):
if not report_sections.get(key):
missing.append(key)
return missing
def _as_list(value: Any) -> list[Any]:
if value is None:
return []
if isinstance(value, list):
return value
if isinstance(value, tuple):
return list(value)
return [value]