报告开头改成重点摘要卡
新报告用围栏画出大运和事业、财富、婚恋,标题下写名字与日期。旧报告不变。 自测: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:
co-authored by
Grok (agent loop)
parent
963276ca2f
commit
8883f8475f
@@ -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)",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user