fix(report): keep the chat annual card's Tajika list, finish KP headings, say the dasha caveat once (BUG-1282)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-08 22:30:33 +08:00
co-authored by Claude Opus 5.5
parent b8eef75871
commit a13f5c2683
9 changed files with 116 additions and 18 deletions
+16
View File
@@ -17278,3 +17278,19 @@
- 相关记录:BUG-1272、BUG-1277。
- 复发自:无。
- 修复版本:本分支,未合入 staging。
## BUG-1282 | 完整数据版分支让对话年运卡的 Tajika 相位变空,附录 KP 标题半译,免责话重复 743 次
- 状态:resolved(分支 `codex/report-full-data-edition-20261007` 合入前由 Claude 验收补修;未上线过)
- 首次发现 / 最近更新:2026-10-08 / 2026-10-08
- 来源:Claude 验收 `36737634`(Linux,屏蔽 PyJHora)。
- 影响面:`scripts/solar_return.py`、`scripts/annual_tajika_pack.py`(对话年运卡与报告共用);完整数据版中文附录标题;大运解释章。
- 现象:① `tests/test_consultation_native_layers.py::test_annual_route_attaches_the_annual_pack_verbatim` 失败:对话年运卡 `tajika_aspects` 为空。② 中文标题「KP 行星 Significators」「KP 宫位 Cusps and Significators」「KP Significators / 行星 宫主」「Annual Tajika瑜伽 Rows」;「Natal / KP Ruling Planets」被译成「本命 / KP 征象星」(主宰星不是征象星)。③ 大运解释里四句免责话各重复 734–743 次。
- 触发条件:生成完整数据版或走对话年运路线。
- 根因:① 分支在 `solar_return_full_report` 结果里加了 `chart`,`annual_tajika_pack._tajika_yogas_field` 见到 `chart` 就改走具名格局权威面,`candidate_yogas` 消失;报告的 Tajika 呈现 / Condition / Forming Pair / 格局对照四节恰恰依赖这条新面。② BUG-1274 让术语替换不再进入单词内部后,标题修复表仍以旧的被改坏拼写(「星座ificators」)为键。③ 模板把免责话写在每个时段里。
- 修复:① `solar_return` 改存 `sr_chart`;年运包保留原 `tajika_yogas`(对话用),新增 `tajika_named_yogas`(具名格局面)与 `tajika_strength`;只在完整数据版报告的副本里用 `tajika_named_yogas` 顶替 `tajika_yogas`(`professional_report_reference._with_report_tajika_view`)。报告中英文与修前逐字节相同。② 标题修复表补新拼写;「Ruling Planets」改译「主宰星」;标题行里拉丁格局名与「瑜伽」之间加空格。③ 章首一句 `DASHA_CHAPTER_NOTE`,各时段不再重复(产品 2026-10-08);章首中文避开清理器会替换的「一定会」。
- 验证:`test_consultation_native_layers` 年运路线 9 条候选格局恢复;`tests/test_annual_tajika_pack.py` 键序(三栏:原 `tajika_yogas, sahams` / 新 `tajika_yogas, tajika_named_yogas, tajika_strength, sahams` / 原因:报告专用两字段);`tests/test_report_full_data_fix.py` 新增标题扫描,`本命 / KP 征象星` → `本命 / KP 主宰星`(三栏:原值 / 新值 / 原因:Ruling Planets 是主宰星)。乔布斯盘中文 35.6 万字(上游 50.9 万的 70%,去掉约 7 万字重复),英文 0 汉字。
- 防复发:上述测试;`_tajika_named_yogas_field` 与 `solar_return` 注释写明不得在 `solar_return_full_report` 结果里放 `chart`。
- 相关记录:BUG-1272、BUG-1274、BUG-1276~1281。
- 复发自:无。
- 修复版本:分支补修提交(见 `git log`)。
+14
View File
@@ -62,6 +62,7 @@ def build_annual_tajika_pack(
"muntha": muntha,
"year_lord": year_lord,
"tajika_yogas": tajika_yogas,
"tajika_named_yogas": _tajika_named_yogas_field(solar_report),
"tajika_strength": _tajika_strength_field(solar_report if isinstance(solar_report, dict) else {}),
"sahams": sahams,
"mudda_dasha": mudda_dasha,
@@ -209,6 +210,19 @@ def _annual_chart_from_solar_report(report: dict[str, Any]) -> dict[str, Any]:
return field
def _tajika_named_yogas_field(solar_report: Any) -> dict[str, Any]:
"""The named-yoga authority surface, for the full-data report only.
`tajika_yogas` keeps the cmd_tajika candidate list that the chat annual card
reads (consultation_native_layers). The report reads this field instead.
"""
chart = solar_report.get("sr_chart") if isinstance(solar_report, dict) else None
if not isinstance(chart, dict) or solar_report.get("error"):
return {"status": "not_applicable", "producer": "Tajika Named Yoga Authority", "reason": "annual_chart_planets_missing"}
return _tajika_yogas_field({**solar_report, "chart": chart})
def _tajika_strength_field(report: dict[str, Any]) -> dict[str, Any]:
"""Copy numeric strength scores. Internal status words stay off this surface."""
+11 -2
View File
@@ -223,8 +223,8 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
text = text.replace("Natural friends", "天然友星")
text = text.replace("Natural enemies", "天然敌星")
text = text.replace("Temporary relation", "临时关系")
text = text.replace("Natal / KP Ruling Planets", "本命 / KP 征象星")
text = text.replace("Ruling Planets", "征象星")
text = text.replace("Natal / KP Ruling Planets", "本命 / KP 主宰星")
text = text.replace("Ruling Planets", "主宰星")
text = text.replace("Sub-Planets", "副行星")
text = text.replace("Planetary", "行星")
text = text.replace("Planets", "行星")
@@ -437,6 +437,13 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
"Bhinnashtakavarga": "Bhinnashtakavarga 单星八分表",
"KP 行星 星座ificators": "KP 行星征象星",
"KP 宫位 Cusps and 星座ificators": "KP 宫头与征象星",
# Since the term pass stopped rewriting inside words (BUG-1274),
# "Significators" arrives intact; these keys match that spelling.
"KP 宫位 Cusps and Significators": "KP 宫头与征象星",
"KP 行星 Significators": "KP 行星征象星",
"KP Significators / 行星 宫主": "KP 征象星 / 行星主星",
"KP Chart / Cusps": "KP 星盘 / 宫头",
"Annual Tajika瑜伽 Rows": "年度 Tajika 瑜伽行",
"KP 星座ificators / 行星 Lords": "KP 征象星 / 行星主星",
"Upagraha / Sub-行星s 点位": "Upagraha / 副行星点位",
"大运 Tables": "Dasha 表格",
@@ -467,6 +474,8 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
text = replace_natural_language_term(text, "house condition", "宫位条件")
for raw, public in zh_heading_repairs.items():
text = text.replace(raw, public)
# Heading lines only: a Latin yoga name keeps a space before 瑜伽 ("Dhana 瑜伽").
text = re.sub(r"(?m)^(#{1,6} .*?[A-Za-z])瑜伽", r"\1 瑜伽", text)
text = replace_natural_language_term(text, " condition", "宫条件")
zh_public_key_repairs = {
"mangal_dosha": "火星婚姻煞",
+14 -11
View File
@@ -339,7 +339,11 @@ def _lord_facts(packet: dict) -> dict[str, dict[str, Any]]:
def _period_beats(lord: str, language: str) -> str:
"""Four traditional beats. Wording stays possible, not certain."""
"""Four traditional beats. Wording stays possible, not certain.
The "possible, not certain" caveat is said once at the top of the Dasha
chapter (DASHA_CHAPTER_NOTE), not repeated in every period (product 2026-10-08).
"""
bank = _PHALA.get(lord) or _PHALA["Saturn"]
topic, good, pressure = bank["en" if language == "en" else "zh"]
@@ -347,26 +351,26 @@ def _period_beats(lord: str, language: str) -> str:
if language == "zh":
return (
f"这段时间可能突出的主题:传统阅读里,{name}容易把{topic}推到眼前。"
"这只是可能被注意到的主题,不是已经发生的事。"
f"条件好时可能:若落宫、尊贵和当前大运同向,可能出现{good}。"
"仍要与宫主、分盘和星宿合看,不能单凭这一层下结论。"
f"受压时可能表现为:若该星受压或与当前节奏不一致,可能感到{pressure}。"
"这是压力下的可能表现,不是确定会发生的事。"
f"怎么细读:先核对{name}的落宫、守护宫、尊贵、星宿和功能吉凶,"
"再看它是否出现在主运、子运或次子运。只把它当成阶段线索。"
"再看它是否出现在主运、子运或次子运。"
)
return (
f"Topics this period may bring forward: in the traditional reading, {name} may bring {topic} forward. "
"These are topics that may be noticed, not events that have already happened. "
f"When conditions are supportive, it may show {good}. "
"House lords, divisional charts, and the nakshatra still have to be read with it. "
f"Under pressure, it may show as {pressure}. "
"That is a possible expression under pressure, not a certain event. "
f"How to read it more closely: check the house, ownership, dignity, nakshatra, and functional nature of {name}, "
"then whether it is the Maha, Antar, or Pratyantar lord. Treat it as a period clue."
"then whether it is the Maha, Antar, or Pratyantar lord."
)
DASHA_CHAPTER_NOTE = {
"zh": "以下每段大运、子运、次子运都只写这段时间可能突出的主题与条件,既不是事实陈述,也不是预言;每一段都要和宫主、分盘、星宿以及当前大运层级合看,不能单凭一段下结论。",
"en": "Every Maha, Antar and Pratyantar section below describes topics and conditions that may come forward in that period, not events that have happened or must happen. Read each with the house lords, divisional charts, nakshatra and the running period level; no single section is a conclusion on its own.",
}
def _ruled_houses_clause(lord: str, owned: list, language: str) -> str:
"""Rahu and Ketu rule no houses. Other empty lists stay an explicit gap."""
@@ -476,6 +480,7 @@ def build_dasha_interpretation_pack(dasha_master_pack: dict[str, Any], profile:
"status": status,
"language": language,
"page_series": pages,
"chapter_note": dict(DASHA_CHAPTER_NOTE),
"domain_paragraphs": _domain_paragraphs(items, language),
"summary": {"page_series_count": len(pages), "narrative_item_count": len(items)},
"audit": {"status": status, "source_dasha_master_status": source_status},
@@ -634,7 +639,6 @@ def _sub_paragraph(parent: str, lord: str, row: dict, facts: dict, language: str
f"{_planet(parent, language)} / {_planet(lord, language)}({level}):{start} 至 {end}。"
f"这段时间可能突出{_planet(lord, language)}的第{house_text}宫、{ruled}、"
f"尊贵{_dignity(facts.get('dignity'), language)}与星宿{facts.get('nakshatra') or '未列出'}。"
"这是阶段线索,不是确定会发生的事。"
+ _period_beats(lord, language)
)
house_text = str(house) if house is not None else "not listed"
@@ -645,7 +649,6 @@ def _sub_paragraph(parent: str, lord: str, row: dict, facts: dict, language: str
f"{parent} / {lord} ({level}): {start} to {end}. "
f"This period may bring forward {lord}'s house {house_text}, {ruled}, "
f"dignity {_dignity(facts.get('dignity'), language)}, and nakshatra {nakshatra}. "
"This is a period clue, not a certain event. "
+ _period_beats(lord, language)
)
+3
View File
@@ -2949,6 +2949,9 @@ def render_pl9_parity_markdown(packet: dict) -> str:
dasha_pages = dasha.get('page_series') if isinstance(dasha.get('page_series'), list) else []
if dasha_pages:
lines.extend(['## Dasha Interpretation' if _lang == 'en' else '## 大运解释', ''])
_note = dasha.get('chapter_note') if isinstance(dasha.get('chapter_note'), dict) else {}
if _note.get(_lang):
lines.extend([str(_note[_lang]), ''])
for page in dasha_pages:
if not isinstance(page, dict):
continue
+18 -1
View File
@@ -104,6 +104,23 @@ def _normalize_languages(value: Any) -> list[str]:
return selected
def _with_report_tajika_view(packet: dict) -> dict:
"""Read Tajika named yogas from their own field in the full-data report.
The shared annual pack keeps `tajika_yogas` as the candidate list the chat
annual card uses; only this report copy swaps in `tajika_named_yogas`.
"""
worksheets = packet.get("worksheets")
timing = worksheets.get("timing_and_predictive_systems") if isinstance(worksheets, dict) else None
annual = timing.get("annual_tajika_pack") if isinstance(timing, dict) else None
named = annual.get("tajika_named_yogas") if isinstance(annual, dict) else None
if not isinstance(named, dict) or named.get("status") == "not_applicable":
return packet
packet["worksheets"] = {**worksheets, "timing_and_predictive_systems": {
**timing, "annual_tajika_pack": {**annual, "tajika_yogas": named}}}
return packet
def _english_edition(packet: dict, include_fact_tables: bool) -> dict[str, Any]:
"""Render the English reader from a deep copy of the same packet.
@@ -194,7 +211,7 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine
raise ProfessionalReportReferenceInputError(str(exc)) from exc
if edition == FULL_DATA_EDITION and isinstance(packet, dict):
packet = dict(packet)
packet = _with_report_tajika_view(dict(packet))
packet["report_version"] = REPORT_VERSION_FULL
if output_format == "markdown":
if edition == FULL_DATA_EDITION:
+6 -3
View File
@@ -670,8 +670,11 @@ def solar_return_full_report(
# 9. 综合总结
result['summary'] = _make_sr_summary(result)
# Keep the longitudes and chart already calculated above so the report
# pack can print them. This does not recompute the return.
# Keep the longitudes already calculated above so the report pack can print
# them. This does not recompute the return. Do not attach `chart` here: a
# `chart` key switches annual_tajika_pack._tajika_yogas_field onto the named-
# yoga authority surface and empties the chat annual card's Tajika aspects.
# The report reads that surface from `tajika_named_yogas` instead.
if not degraded:
result['planet_lons'] = {
name: float(value)
@@ -680,7 +683,7 @@ def solar_return_full_report(
}
chart = sr.get('chart') if isinstance(sr, dict) else None
if isinstance(chart, dict):
result['chart'] = chart
result['sr_chart'] = chart
return result
+4
View File
@@ -40,6 +40,10 @@ def test_builds_annual_tajika_pack_contract() -> None:
"muntha",
"year_lord",
"tajika_yogas",
# Full-data report only (2026-10-08): the named-yoga authority surface
# and the Tajika strength scores. `tajika_yogas` stays the chat card's list.
"tajika_named_yogas",
"tajika_strength",
"sahams",
"mudda_dasha",
"patyayini_dasha",
+30 -1
View File
@@ -160,7 +160,8 @@ def test_t5_repairs_bhava_bala_and_planets_suffix() -> None:
assert "六维力量 / Bhava Bala" in text
assert "Bhava Bala 十二宫力量" in text
assert "行星s" not in text
assert "本命 / KP 征象星" in text
# Ruling planets are not significators (Claude acceptance 2026-10-08): was 本命 / KP 征象星.
assert "本命 / KP 主宰星" in text
assert HALF_TRANSLATED.search(text) is None
assert "年龄状态" not in text.split("Graha Avastha", 1)[-1] or "Baladi" not in raw
@@ -292,3 +293,31 @@ def test_chinese_render_stays_stable_when_english_uses_a_copy() -> None:
render_full_data_markdown(english)
after = render_full_data_markdown(copy.deepcopy(packet))
assert after == before
def test_zh_headings_carry_no_untranslated_kp_or_row_words() -> None:
# Claude acceptance 2026-10-08: after the term pass stopped rewriting inside
# words, appendix headings such as "KP 行星 Significators" and
# "Annual Tajika瑜伽 Rows" were left half translated.
raw = "\n".join([
"# 印度占星星盘报告",
"",
"## KP Planet Significators",
"",
"## KP House Cusps and Significators",
"",
"### KP Significators / Planet Lords",
"",
"### KP Chart / Cusps",
"",
"## Annual Tajika Yoga Rows",
"",
"#### Dhana Yoga",
"",
])
text = _sanitize_pl9_ai_density_markdown(raw)
headings = re.findall(r"(?m)^#{2,6} (.*)$", text)
pattern = re.compile(r"\b(Significators|Cusps|Rows|Lords|Ruling|Planets?)\b|[A-Za-z]{3,}[\u4e00-\u9fff]|[\u4e00-\u9fff]s\b")
assert [h for h in headings if pattern.search(h)] == []
assert "KP 行星征象星" in headings and "KP 宫头与征象星" in headings and "年度 Tajika 瑜伽行" in headings