fix(engine): keep the Chinese report free of English glue from #166 triggers and cell notes (BUG-1596, BUG-1597, BUG-1590)

Quick gate: test_report_english_sweep_smoke counted 3 zh_han_function_glue
hits on chart #1, all new in 4b223eb8: the Matrunasa 199 trigger "Moon ->
Mercury -> Moon in house 8" (the renderer turns "house" into Chinese and
leaves "in") and the English ambiguous_cell_notes ("Lagna lord, ...").

- A custom rule's trigger item may be a (Chinese, English) pair; the Chinese
  combination uses the Chinese items, combination_en and `triggers` the
  English ones, `triggers_zh` the Chinese. Gajakesari, the variant and both
  Matrunasa rules emit pairs (PLANET_ZH in the sandbox).
- ambiguous_cell_notes carry Chinese summary / basis plus summary_en /
  basis_en, worded without the glue words.
No role or score changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
This commit is contained in:
2026-11-04 22:39:15 +08:00
co-authored by Claude Opus 5.5
parent f61d279aec
commit 7ff447ac67
4 changed files with 30 additions and 18 deletions
+4 -4
View File
@@ -946,7 +946,7 @@
},
{
"type": "custom",
"expr": "\n# #166 Q5 (BUG-1596): PVR strict definition. Jupiter in a kendra from the Moon (previous condition),\n# conjoined or aspected by a benefic, not debilitated, not combust, not in a natural enemy's sign.\n# The strong-Jupiter substitute is a separate rule (gajakesari_strong_jupiter_variant).\ntriggers = []\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nok = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nif ok:\n triggers = ['benefic association: ' + ', '.join(benefic_links)]\n__result__ = ok\n",
"expr": "\n# #166 Q5 (BUG-1596): PVR strict definition. Jupiter in a kendra from the Moon (previous condition),\n# conjoined or aspected by a benefic, not debilitated, not combust, not in a natural enemy's sign.\n# The strong-Jupiter substitute is a separate rule (gajakesari_strong_jupiter_variant).\ntriggers = []\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nok = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nif ok:\n triggers = [('吉星关联:' + '、'.join(PLANET_ZH.get(p, p) for p in benefic_links), 'benefic association: ' + ', '.join(benefic_links))]\n__result__ = ok\n",
"combo_template": "严格定义:木星受吉星合相或相照,且不落陷、不燃烧、不落敌宫",
"combo_template_en": "Strict definition: Jupiter conjoined or aspected by a benefic, not debilitated, combust or in an enemy sign"
}
@@ -990,7 +990,7 @@
},
{
"type": "custom",
"expr": "\n# #166 Q5: variant only when the strict definition does not hold.\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nstrict = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nstrong_jup = exalted('Jupiter') or own('Jupiter') or moola('Jupiter')\ntriggers = [f\"strong Jupiter: {dignity('Jupiter')}\"] if strong_jup and not strict else []\n__result__ = strong_jup and not strict and not debil('Jupiter')\n",
"expr": "\n# #166 Q5: variant only when the strict definition does not hold.\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nstrict = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nstrong_jup = exalted('Jupiter') or own('Jupiter') or moola('Jupiter')\ntriggers = [(f\"木星强:{dignity('Jupiter')}\", f\"strong Jupiter: {dignity('Jupiter')}\")] if strong_jup and not strict else []\n__result__ = strong_jup and not strict and not debil('Jupiter')\n",
"combo_template": "变体:木星强(入旺 / 本宫 / 根本三角)代替吉星关联",
"combo_template_en": "Variant: a strong Jupiter (exalted / own / moolatrikona) in place of benefic association"
}
@@ -11578,7 +11578,7 @@
"category": "durbhaga",
"logic": {
"type": "custom",
"expr": "\n# #166 Q6 (BUG-1597): Raman 300 Combinations no. 198 only (no. 199 is bvr_matrunasa_199).\n# Hemmed by, associated with, or aspected by natural malefics (OR). Aspects are named Parashari graha\n# aspects (7th; Mars 4/8, Jupiter 5/9, Saturn 3/10); Rahu / Ketu count for conjunction and hemming,\n# not for aspect (implementation choice recorded in BUG-1597). No Jaimini rasi drishti.\ntriggers = []\nif 'Moon' in ctx.planets:\n moon_h = house_of('Moon')\n prev_h = ((moon_h - 2) % 12) + 1\n next_h = (moon_h % 12) + 1\n prev_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == prev_h]\n next_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == next_h]\n if prev_m and next_m:\n triggers.append('hemmed: ' + ', '.join(prev_m + next_m))\n conj = [p for p in MALEFICS if p in ctx.planets and same_house('Moon', p)]\n if conj:\n triggers.append('with Moon: ' + ', '.join(conj))\n asp = [p for p in ('Sun', 'Mars', 'Saturn') if p in ctx.planets and not same_house('Moon', p) and aspect(p, 'Moon')]\n if asp:\n triggers.append('aspects Moon: ' + ', '.join(asp))\n__result__ = bool(triggers)\n",
"expr": "\n# #166 Q6 (BUG-1597): Raman 300 Combinations no. 198 only (no. 199 is bvr_matrunasa_199).\n# Hemmed by, associated with, or aspected by natural malefics (OR). Aspects are named Parashari graha\n# aspects (7th; Mars 4/8, Jupiter 5/9, Saturn 3/10); Rahu / Ketu count for conjunction and hemming,\n# not for aspect (implementation choice recorded in BUG-1597). No Jaimini rasi drishti.\ntriggers = []\nif 'Moon' in ctx.planets:\n moon_h = house_of('Moon')\n prev_h = ((moon_h - 2) % 12) + 1\n next_h = (moon_h % 12) + 1\n prev_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == prev_h]\n next_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == next_h]\n if prev_m and next_m:\n triggers.append(('凶星夹制:' + '、'.join(PLANET_ZH.get(p, p) for p in prev_m + next_m), 'hemmed: ' + ', '.join(prev_m + next_m)))\n conj = [p for p in MALEFICS if p in ctx.planets and same_house('Moon', p)]\n if conj:\n triggers.append(('与月亮同宫:' + '、'.join(PLANET_ZH.get(p, p) for p in conj), 'with Moon: ' + ', '.join(conj)))\n asp = [p for p in ('Sun', 'Mars', 'Saturn') if p in ctx.planets and not same_house('Moon', p) and aspect(p, 'Moon')]\n if asp:\n triggers.append(('相照月亮:' + '、'.join(PLANET_ZH.get(p, p) for p in asp), 'aspects Moon: ' + ', '.join(asp)))\n__result__ = bool(triggers)\n",
"combo_template": "Raman 198:月亮受凶星夹制、合相或相照",
"combo_template_en": "Raman 198: Moon hemmed, conjoined or aspected by malefics"
},
@@ -11603,7 +11603,7 @@
"category": "durbhaga",
"logic": {
"type": "custom",
"expr": "\n# #166 Q6: Raman no. 199. Lord of the D9 sign holding the 4th lord; then the lord of the D9 sign\n# that planet holds; that final planet in the 6th / 8th / 12th of D1.\ntriggers = []\nl4 = lord(4)\nnav_lord = navamsa_dispositor(l4) if l4 else None\nfinal_planet = navamsa_dispositor(nav_lord) if nav_lord else None\nif final_planet in ctx.planets and house_of(final_planet) in [6, 8, 12]:\n triggers.append(f'{l4} -> {nav_lord} -> {final_planet} in house {house_of(final_planet)}')\n__result__ = bool(triggers)\n",
"expr": "\n# #166 Q6: Raman no. 199. Lord of the D9 sign holding the 4th lord; then the lord of the D9 sign\n# that planet holds; that final planet in the 6th / 8th / 12th of D1.\ntriggers = []\nl4 = lord(4)\nnav_lord = navamsa_dispositor(l4) if l4 else None\nfinal_planet = navamsa_dispositor(nav_lord) if nav_lord else None\nif final_planet in ctx.planets and house_of(final_planet) in [6, 8, 12]:\n triggers.append((f'{PLANET_ZH.get(l4, l4)} → {PLANET_ZH.get(nav_lord, nav_lord)} → {PLANET_ZH.get(final_planet, final_planet)},落第 {house_of(final_planet)} 宫', f'{l4} -> {nav_lord} -> {final_planet}, house {house_of(final_planet)}'))\n__result__ = bool(triggers)\n",
"combo_template": "Raman 199:4宫主的 Navamsa 链最终落 6 / 8 / 12 宫",
"combo_template_en": "Raman 199: the 4th lord's Navamsa chain ends in the 6th / 8th / 12th"
},
+10 -10
View File
@@ -53,14 +53,14 @@ ROLE_OVERRIDES = {
# summary; the other lordship is kept as a duty, never dropped. Basis says whether
# the sign's own verse states it or it is derived from the general rules.
AMBIGUOUS_CELL_NOTES = {
("Taurus", "Mercury"): ("mildly benefic; keeps 2nd-lord duties", "sign verse: somewhat auspicious"),
("Gemini", "Mercury"): ("Lagna lord, supportive; not a Yogakaraka", "general rules"),
("Gemini", "Saturn"): ("mixed; 9th-lord support, not a pure benefic", "general rules with sign context"),
("Sagittarius", "Saturn"): ("challenging (3rd lord); Maraka (2nd lord) listed separately", "3rd-house rule plus sign verse"),
("Cancer", "Jupiter"): ("mildly benefic; keeps 6th-lord duties", "sign verse"),
("Leo", "Jupiter"): ("mildly benefic; keeps 8th-lord duties", "sign verse"),
("Virgo", "Saturn"): ("mixed; not a pure benefic on the 5th lordship alone", "sign verse does not name it a benefic"),
("Capricorn", "Mercury"): ("mildly benefic; keeps 6th-lord duties", "sign verse"),
("Taurus", "Mercury"): ("偏吉,保留 2 宫主职责", "专条:有所吉利", "mildly benefic; keeps 2nd-lord duties", "sign verse: somewhat auspicious"),
("Gemini", "Mercury"): ("上升主,支持;不自动认定 Yogakaraka", "通用规则推导", "Ascendant ruler, supportive; not a Yogakaraka", "general rules"),
("Gemini", "Saturn"): ("混合,9 宫支持,不标纯吉", "通用规则与专条语境并列", "mixed; 9th-house support, not a pure benefic", "general rules with sign context"),
("Sagittarius", "Saturn"): ("功能挑战(3 宫主);Maraka(2 宫主)另列", "3 宫通则加专条身份", "challenging (3rd house); Maraka (2nd house) listed separately", "3rd-house rule plus sign verse"),
("Cancer", "Jupiter"): ("偏吉,保留 6 宫主职责", "专条明确支持", "mildly benefic; keeps 6th-house duties", "sign verse"),
("Leo", "Jupiter"): ("偏吉,保留 8 宫主职责", "专条明确支持", "mildly benefic; keeps 8th-house duties", "sign verse"),
("Virgo", "Saturn"): ("混合,不只凭 5 宫判纯吉", "专条未明确列为吉星", "mixed; not a pure benefic on the 5th house alone", "sign verse does not name it a benefic"),
("Capricorn", "Mercury"): ("偏吉,保留 6 宫主职责", "专条明确支持", "mildly benefic; keeps 6th-house duties", "sign verse"),
}
@@ -158,8 +158,8 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]:
"rule_reference": "references/functional-role-profile.md",
"assertion_label": "parameter_sensitive",
"ambiguous_cell_notes": {
planet: {"summary": note, "basis": basis}
for (sign, planet), (note, basis) in AMBIGUOUS_CELL_NOTES.items() if sign == asc_sign
planet: {"summary": note, "basis": basis, "summary_en": note_en, "basis_en": basis_en}
for (sign, planet), (note, basis, note_en, basis_en) in AMBIGUOUS_CELL_NOTES.items() if sign == asc_sign
},
"role_basis": {
planet: "BPHS ch34 sign-specific exception" if planet in ROLE_OVERRIDES.get(asc_sign, {})
+14 -4
View File
@@ -65,6 +65,9 @@ NATURAL_ENEMIES = {
# Combustion orbs (direct motion), same values as jyotish_engine.COMBUSTION_ORBS
# direct column; used only when the chart row carries no `combust` flag.
COMBUSTION_ORBS_DIRECT = {'Moon': 12.0, 'Mars': 17.0, 'Mercury': 14.0, 'Jupiter': 11.0, 'Venus': 10.0, 'Saturn': 15.0}
# Short Chinese planet names for bilingual rule triggers (#166 Q5 / Q6).
PLANET_ZH = {'Sun': '太阳', 'Moon': '月亮', 'Mars': '火星', 'Mercury': '水星', 'Jupiter': '木星',
'Venus': '金星', 'Saturn': '土星', 'Rahu': '罗睺', 'Ketu': '计都'}
PLANET_CN = {
"Ketu": "南交点Ketu", "Venus": "金星Venus", "Sun": "太阳Sun",
"Moon": "月亮Moon", "Mars": "火星Mars", "Rahu": "北交点Rahu",
@@ -823,6 +826,7 @@ class YogaEngine:
"category": rule.get("category", ""),
"rule_id": rule.get("id", ""),
**({"triggers": m["triggers"]} if m.get("triggers") else {}),
**({"triggers_zh": m["triggers_zh"]} if m.get("triggers_zh") else {}),
**({"canonical_mapping": rule["canonical_mapping"]} if rule.get("canonical_mapping") else {}),
# English twins for the reader's English edition; the Chinese
# fields above are unchanged.
@@ -1011,7 +1015,8 @@ class YogaEngine:
"combination": merged_combo,
"strength": sm.get("strength", match.get("strength")),
"bindings": {**match.get("bindings", {}), **sm.get("bindings", {})},
**({"triggers": (match.get("triggers") or []) + (sm.get("triggers") or [])}
**({"triggers": (match.get("triggers") or []) + (sm.get("triggers") or []),
"triggers_zh": (match.get("triggers_zh") or []) + (sm.get("triggers_zh") or [])}
if match.get("triggers") or sm.get("triggers") else {}),
})
results = new_results
@@ -1538,11 +1543,15 @@ class YogaEngine:
strength = strength(ctx, bindings)
# #166 Q6 (BUG-1597): a rule may set `triggers` (list of short strings)
# to name what actually fired; they are appended to the combination.
# An item is either one string (used in both editions) or a
# (Chinese, English) pair, so the Chinese report never carries English glue.
triggers = exec_globals.get("triggers")
if isinstance(triggers, list) and triggers:
triggers = [str(item) for item in triggers]
combo = BiText(f"{combo}:{';'.join(triggers)}", f"{_en_text(combo)}: {'; '.join(triggers)}")
return [{"combination": combo, "strength": strength, "triggers": triggers}]
pairs = [(item[0], item[1]) if isinstance(item, (list, tuple)) and len(item) == 2 else (item, item) for item in triggers]
zh_items = [str(zh) for zh, _ in pairs]
en_items = [str(en) for _, en in pairs]
combo = BiText(f"{combo}:{';'.join(zh_items)}", f"{_en_text(combo)}: {'; '.join(en_items)}")
return [{"combination": combo, "strength": strength, "triggers": en_items, "triggers_zh": zh_items}]
return [{"combination": combo, "strength": strength}]
return []
@@ -1835,6 +1844,7 @@ class YogaEngine:
"house_strength": house_strength, "strong": strong, "weak": weak,
"associated": associated, "temporal_friend": temporal_friend, "natural_friend": natural_friend,
"combust": combust, "in_natural_enemy_sign": in_natural_enemy_sign,
"PLANET_ZH": PLANET_ZH,
"occupants": occupants, "only_benefics_in_house": only_benefics_in_house,
"only_malefics_in_house": only_malefics_in_house,
"house_has_benefic": house_has_benefic, "house_has_malefic": house_has_malefic,
@@ -122,6 +122,7 @@ def test_q5_strict_gajakesari_holds_with_benefic_and_clean_jupiter() -> None:
assert "Gajakesari Yoga" in rows
# Venus conjoined; Mercury in Virgo aspects Pisces (7th).
assert rows["Gajakesari Yoga"]["triggers"] == ["benefic association: Venus, Mercury"]
assert rows["Gajakesari Yoga"]["triggers_zh"] == ["吉星关联:金星、水星"]
assert "Strong-Jupiter Gajakesari Variant" not in rows
@@ -142,4 +143,5 @@ def test_q6_matrunasa_198_rasi_drishti_alone_no_longer_fires_and_triggers_are_na
rows = {row["name"]: row for row in _detect(planets, "Aries")}
hit = rows.get("Matrunasa Yoga")
assert hit is not None and hit["triggers"] == ["aspects Moon: Saturn"]
assert hit["triggers_zh"] == ["相照月亮:土星"]
assert hit["effects"] and "健康" not in hit["effects"][0].replace("不作母亲健康", "")