报告开头改成重点摘要卡

新报告用围栏画出大运和事业、财富、婚恋,标题下写名字与日期。旧报告不变。

自测:pytest test_report_full_data_fix.py 10 通过;相关四份报告测试退出码 0;前端相关测试 147 通过、0 失败、0 取消;tsc 退出码 0;lint 0 error。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-13 21:45:43 +08:00
co-authored by Grok (agent loop)
parent 963276ca2f
commit 8883f8475f
20 changed files with 856 additions and 82 deletions
+48 -12
View File
@@ -91,7 +91,12 @@ def _load_bhava_rows():
return bhava_house_rows
def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星星盘报告") -> str:
def _sanitize_pl9_ai_density_markdown(
markdown: str,
title: str = "印度占星星盘报告",
*,
opening_summary: bool = True,
) -> str:
"""Keep the parity volume's facts while removing internal-only labels."""
try:
@@ -111,6 +116,8 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
from pl9_language_terms import translate_zh_glued_phrases
except ModuleNotFoundError: # pragma: no cover - package import compatibility
from scripts.pl9_language_terms import translate_zh_glued_phrases
hold_focus_fences, restore_focus_fences = _focus_fence_tools()
markdown, focus_slots = hold_focus_fences(markdown)
# Whole phrases and glued planet+dignity tokens before shorter word swaps.
# Chapters appended after the parity strip still pass through here.
text = translate_zh_glued_phrases(_sanitize_pl9_public_markdown(markdown))
@@ -515,10 +522,12 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
continue
first_body_line = line
break
has_opening_summary = bool(first_body_line) and any(
marker in first_body_line for marker in ("摘要:", "结论:", "重点:")
has_opening_summary = _opening_focus_authored(text) or (
bool(first_body_line) and any(
marker in first_body_line for marker in ("摘要:", "结论:", "重点:")
)
)
if text.startswith(f"# {title}") and not has_opening_summary:
if opening_summary and text.startswith(f"# {title}") and not has_opening_summary:
summary = "\n".join(
[
f"# {title}",
@@ -532,7 +541,7 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
]
)
text = re.sub(r"^#\s+.*$", summary, text, count=1, flags=re.MULTILINE)
return strip_han_plural_s(text) + "\n"
return restore_focus_fences(strip_han_plural_s(text) + "\n", focus_slots)
@@ -887,7 +896,8 @@ def _sanitize_pl9_ai_density_markdown_en(markdown: str, title: str = "Vedic Astr
except ModuleNotFoundError: # pragma: no cover - package import compatibility
from scripts.pl9_language_terms import normalize_en_markdown_terms
text = markdown
hold_focus_fences, restore_focus_fences = _focus_fence_tools()
text, focus_slots = hold_focus_fences(markdown)
replacements = {
"# PL9 对标主册": f"# {title}",
"PL9 对标主册": title,
@@ -1266,7 +1276,7 @@ def _sanitize_pl9_ai_density_markdown_en(markdown: str, title: str = "Vedic Astr
# 防御性幂等:折叠残留的 "condition condition"(确保 sanitize 可安全重复执行)。
text = re.sub(r"\b(condition)(?:\s+condition)+\b", r"\1", text, flags=re.IGNORECASE)
text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
return text
return restore_focus_fences(text, focus_slots)
@@ -1279,7 +1289,10 @@ _ZH_FUNCTION_WORD_RE = re.compile(
r"(?<![A-Za-z])(?:of|in|the|and|by|from|with|listed|background|native|condition|met|satisfied|occupied)(?![A-Za-z])",
re.IGNORECASE,
)
_ZH_CHART_FENCE_RE = re.compile(r"```jyotish-chart\n.*?\n```", re.DOTALL)
_ZH_CHART_FENCE_RE = re.compile(
r"```(?:jyotish-chart|jyotish-focus)\n.*?\n```",
re.DOTALL,
)
def _zh_body_residue(markdown: str) -> dict[str, int]:
@@ -2230,14 +2243,36 @@ def _append_native_rashi_dashas(markdown: str, packet: dict) -> str:
return markdown.rstrip() + "\n\n" + chapter
def _focus_fence_tools():
try:
from report_focus_block import hold_focus_fences, restore_focus_fences
except ModuleNotFoundError: # pragma: no cover - package import compatibility
from scripts.report_focus_block import hold_focus_fences, restore_focus_fences
return hold_focus_fences, restore_focus_fences
def _opening_focus_authored(text: str) -> bool:
"""True when the title is already followed by the focus heading or fence.
The fixed checklist must not come back just because the old one-line
「重点:」 summary was removed.
"""
head = "\n".join((text or "").splitlines()[:16])
if re.search(r"(?m)^## (?:重点|Focus)\s*$", head):
return True
return "```jyotish-focus" in head or "JYOTISHFOCUSHOLD" in head
def _insert_opening_focus(markdown: str, focus: str) -> str:
"""Put the current-period focus directly under the title."""
first = next((line.strip() for line in focus.splitlines() if line.strip()), "")
if not first or first in markdown:
if "```jyotish-focus" not in (focus or ""):
return markdown
match = re.search(r"^#\s+.+$", markdown, flags=re.MULTILINE)
if "```jyotish-focus" in (markdown or ""):
return markdown
match = re.search(r"^#\s+.+$", markdown or "", flags=re.MULTILINE)
if not match:
return focus.rstrip() + "\n\n" + markdown
return focus.rstrip() + "\n\n" + (markdown or "")
return markdown[: match.end()] + "\n\n" + focus.rstrip() + "\n" + markdown[match.end() :]
@@ -2286,6 +2321,7 @@ def render_full_data_markdown(packet: dict) -> str:
zh_structured_markdown = _sanitize_pl9_ai_density_markdown(
_render_pl9_ai_density_raw_data_markdown_en(packet),
_pl9_public_report_title(packet),
opening_summary=False,
)
zh_structured_appendix = re.sub(
r"^#\s+.*?(?:\n{2,}|\n)",
+75 -44
View File
@@ -1393,61 +1393,92 @@ def missing_field_count(packet: dict) -> int:
return len(fields)
def _iso_day(value: Any) -> str | None:
text = _date_text(value)
return text if re.fullmatch(r"\d{4}-\d{2}-\d{2}", text) else None
def _current_period_rows(packet: dict) -> dict[str, dict]:
"""Current MD/AD/PD rows already stored on the dasha timeline. No new dates."""
dasha = _dict(_timing(packet).get("dasha"))
current = _dict(dasha.get("current_dasha"))
md_row = current if current.get("lord") else {}
if not md_row:
for period in _list(dasha.get("timeline")):
if isinstance(period, dict) and period.get("is_current"):
md_row = period
break
ad_row: dict = {}
pd_row: dict = {}
for row in _list(md_row.get("antardasha_timeline")):
if isinstance(row, dict) and row.get("is_current"):
ad_row = row
for child in _list(row.get("pratyantar_dasha_timeline")):
if isinstance(child, dict) and child.get("is_current"):
pd_row = child
break
break
return {"md": md_row, "ad": ad_row, "pd": pd_row}
def _focus_fence(payload: dict) -> str:
try:
from report_focus_block import build_focus_fence
except ModuleNotFoundError: # pragma: no cover - package import compatibility
from scripts.report_focus_block import build_focus_fence
return build_focus_fence(payload)
def opening_focus_block(packet: dict) -> str:
"""Current MD/AD/PD plus career, wealth, and relationship house lords."""
"""Current MD/AD/PD plus career, wealth, and relationship house lords.
The reader draws the card from the fence. This function does not compute
a new dasha or house lord; missing dates stay null.
"""
language = _lang(packet)
lords = _current_lords(packet)
houses = _houses(packet)
planets = _planets(packet)
classification = _functional_classification(packet)
def lord_line(number: int, topic_zh: str, topic_en: str) -> str:
rows = _current_period_rows(packet)
periods = []
for level in ("md", "ad", "pd"):
row = rows[level]
lord = str(row.get("lord") or "").strip()
periods.append({
"level": level,
"lord": lord or None,
"start": _iso_day(row.get("start")),
"end": _iso_day(row.get("end")),
})
domains = []
for key, number in (("career", 10), ("wealth", 2), ("relationship", 7)):
house = _house_row(houses, number)
lord = str(house.get("lord") or "")
lord = str(house.get("lord") or "").strip()
row = planets.get(lord) if isinstance(planets.get(lord), dict) else {}
placed = row.get("house")
dignity = _dignity(row.get("status") or row.get("dignity"), language)
role = _role(classification.get(lord), language)
if language == "zh":
return (
f"- {topic_zh}第{number}宫主 {_planet(lord, language)} 落在第{placed if isinstance(placed, int) else '未列出'}宫,"
f"尊贵为{dignity},功能吉凶为{role}。"
)
return (
f"- {topic_en} house {number} lord {lord or 'not listed'} is in house "
f"{placed if isinstance(placed, int) else 'not listed'}, dignity {dignity}, functional nature {role}."
)
domains.append({
"key": key,
"house": number,
"lord": lord or None,
"placed": placed if isinstance(placed, int) else None,
"dignity": _dignity(row.get("status") or row.get("dignity"), language),
"role": _role(classification.get(lord), language),
})
if language == "zh":
lines = [
"重点:当前主运 MD {md},子运 AD {ad},次子运 PD {pd}。".format(
md=_planet(lords["md"], language),
ad=_planet(lords["ad"], language),
pd=_planet(lords["pd"], language),
),
"",
"## 重点",
lord_line(10, "事业", "Career"),
lord_line(2, "财富", "Resources"),
lord_line(7, "婚恋", "Relationship"),
"- 这段时间可能突出这些宫主所守护的主题,不是确定会发生的事。",
]
caveat = "这段时间可能突出这些宫主所守护的主题,不是确定会发生的事。"
heading = "## 重点"
else:
lines = [
"Focus: current Maha Dasha MD {md}, Antar Dasha AD {ad}, Pratyantar Dasha PD {pd}.".format(
md=lords["md"] or "not listed",
ad=lords["ad"] or "not listed",
pd=lords["pd"] or "not listed",
),
"",
"## Focus",
lord_line(10, "事业", "Career"),
lord_line(2, "财富", "Resources"),
lord_line(7, "婚恋", "Relationship"),
"- This period may bring those house-lord topics forward. It is not a certain event.",
]
return "\n".join(lines)
caveat = "This period may bring those house-lord topics forward. It is not a certain event."
heading = "## Focus"
fence = _focus_fence({
"version": 1,
"periods": periods,
"domains": domains,
"caveat": caveat,
})
return f"{heading}\n\n{fence}"
def _strength_snapshot(packet: dict) -> dict:
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Structured opening-focus fence for the full-data report.
The engine writes one ``jyotish-focus`` JSON fence. The report page renders
that fence; it does not recompute dasha lords or house lords.
"""
from __future__ import annotations
import json
import re
from typing import Any
FOCUS_FENCE_RE = re.compile(r"```jyotish-focus\n(.*?)\n```", re.DOTALL)
_FOCUS_BLOCK_RE = re.compile(r"```jyotish-focus\n.*?\n```", re.DOTALL)
_HOLD_TOKEN = "JYOTISHFOCUSHOLD{index}END"
def build_focus_fence(payload: dict[str, Any]) -> str:
"""Return a one-line JSON fence. Values are already public words."""
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
return f"```jyotish-focus\n{body}\n```"
def parse_focus_fence(markdown: str) -> dict[str, Any] | None:
match = FOCUS_FENCE_RE.search(markdown or "")
if not match:
return None
try:
value = json.loads(match.group(1))
except json.JSONDecodeError:
return None
return value if isinstance(value, dict) else None
def hold_focus_fences(markdown: str) -> tuple[str, list[str]]:
"""Lift fences out of the glossary pass so planet words stay valid JSON."""
slots: list[str] = []
def repl(match: re.Match[str]) -> str:
slots.append(match.group(0))
return _HOLD_TOKEN.format(index=len(slots) - 1)
return _FOCUS_BLOCK_RE.sub(repl, markdown or ""), slots
def restore_focus_fences(markdown: str, slots: list[str]) -> str:
text = markdown
for index, slot in enumerate(slots):
text = text.replace(_HOLD_TOKEN.format(index=index), slot)
return text