diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 5a6348ff..e2e95680 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -17720,3 +17720,18 @@ - 复发自:BUG-1306。 - 修复版本:分支 `codex/rectification-step0-entry-20261013`,尚未合入 staging。 +## BUG-1318 | 婚恋评级仍写成稳定或婚前咨询,配偶星分数被写成百分制 + +- 状态:fixed-pending-verify +- 首次发现 / 最近更新:2026-10-14 / 2026-10-14 +- 来源:issue #56。上游 `f891a2c1`。任务书 `docs/tasks/TASK-upstream-sync7-20261014.md` T1。 +- 现象:D9 综合评级仍写「婚姻稳定」「强烈建议婚前咨询」。这份文字进全读导出包,模型能读到。配偶星证据行把 0 到 1 的指数写成「婚姻质量 x/100」。 +- 触发条件:算出婚恋几何指数,或关系时机证据带出配偶星。 +- 根因:同步第六轮按标题筛提交时,漏了 10-02 到 10-03 的这条措辞修正。分数行把 0 到 1 当成了百分制。 +- 修复:五档改成证据档位,并写明这是结构指数,不预测婚姻次数、稳定、离婚、伴侣好坏,也不提示婚前咨询。中文报告用中文档位,英文报告用英文档位。证据行改成「婚姻质量指数 x(0到1)」,不乘 100。 +- 验证:`tests/test_d9_evidence_bands.py` 3 通过。公开盘中英文各一条快照。配偶星行不再出现 `/100`。 +- 防复发:五档文字不得含「稳定、离婚、再婚、婚前咨询、几段」;快照锁住公开盘的两句。 +- 相关记录:BUG-1183。 +- 复发自:无。 +- 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。 + diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 6fc0e672..c915df6e 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -7551,7 +7551,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM evidence.append({ 'label': 'Darakaraka (DK)', 'value': f"{dk_planet} · H{darakaraka.get('dk_house', '-')}", - 'note': f"{darakaraka.get('core_profile') or '配偶象征星'};婚姻质量 {darakaraka.get('marriage_quality_score', '-')}/100", + 'note': f"{darakaraka.get('core_profile') or '配偶象征星'};婚姻质量指数 {darakaraka.get('marriage_quality_score', '-')}(0到1)", }) except Exception as exc: darakaraka = {'error': str(exc), 'dk_planet': dk_planet} diff --git a/scripts/marriage_counting.py b/scripts/marriage_counting.py index 8cad913c..fca23e04 100644 --- a/scripts/marriage_counting.py +++ b/scripts/marriage_counting.py @@ -21,6 +21,23 @@ from typing import Dict, Optional, List SIGN_CN = ['白羊座','金牛座','双子座','巨蟹座','狮子座','处女座', '天秤座','天蝎座','射手座','摩羯座','水瓶座','双鱼座'] +# D9 结构证据档位(BUG-1318)。中文给报告正文,英文键与英文短句给英文版。 +# 不写婚姻稳定、离婚、再婚、段数或婚前咨询。 +D9_CLAIM_BOUNDARY = ( + 'D9 结构证据指数,不预测婚姻次数、稳定、离婚、伴侣好坏,也不提示需要婚前咨询' +) +D9_CLAIM_BOUNDARY_EN = ( + 'D9 structural evidence index. It does not predict marriage count, stability, ' + 'divorce, or partner quality, and it does not suggest premarital counseling.' +) +D9_EVIDENCE_BANDS = ( + ('supportive_evidence_stronger', '支持证据较强', 'Stronger supportive evidence'), + ('supportive_evidence_mixed_positive', '支持证据偏正', 'Mixed supportive evidence'), + ('mixed_evidence', '证据混杂', 'Mixed evidence'), + ('pressure_evidence_mixed', '压力证据偏杂', 'Mixed pressure evidence'), + ('pressure_evidence_stronger', '压力证据较强', 'Stronger pressure evidence'), +) + def sign_of(lon: float) -> int: """从黄道经度计算星座序号 (0-11)""" @@ -348,25 +365,42 @@ def _assess_d9_marriage_quality( quality_points -= 1 factors.append("D9 Ketu在7宫 → 关系中的分离倾向") - # 综合评级 + # 证据档位,不是婚姻结局(BUG-1318,上游 f891a2c1)。 if quality_points >= 4: - rating = "高(关系质量好,伴侣支持强,婚姻稳定)" + band = D9_EVIDENCE_BANDS[0] elif quality_points >= 1: - rating = "中上(关系质量尚可,需经营但基础好)" + band = D9_EVIDENCE_BANDS[1] elif quality_points >= 0: - rating = "中(关系质量一般,需用心经营)" + band = D9_EVIDENCE_BANDS[2] elif quality_points >= -2: - rating = "中下(关系有挑战,需提前沟通和调整期望)" + band = D9_EVIDENCE_BANDS[3] else: - rating = "低(关系质量有重大挑战,强烈建议婚前咨询)" + band = D9_EVIDENCE_BANDS[4] return { - 'quality_rating': rating, + 'quality_band': band[0], + 'quality_rating': band[1], + 'quality_rating_en': band[2], 'quality_points': quality_points, 'factors': factors, + 'claim_boundary': D9_CLAIM_BOUNDARY, + 'claim_boundary_en': D9_CLAIM_BOUNDARY_EN, } +def render_d9_evidence_report(quality: Dict, language: str = 'zh') -> str: + """中文报告用证据档位短句;英文版用英文短句。两边都带同一条边界。""" + if not isinstance(quality, dict): + quality = {} + if language == 'en': + band = quality.get('quality_rating_en') or quality.get('quality_band') or '' + boundary = quality.get('claim_boundary_en') or D9_CLAIM_BOUNDARY_EN + return f"D9 evidence band: {band}\n{boundary}" + band = quality.get('quality_rating') or '' + boundary = quality.get('claim_boundary') or D9_CLAIM_BOUNDARY + return f"D9 证据档位:{band}\n{boundary}" + + def _marriage_recommendations(base: Dict, d9_quality: Dict) -> List[str]: """几何指数与伴侣数量、事件断语分开(BUG-1183);不随指数大小给不同结论。""" return [ diff --git a/tests/test_d9_evidence_bands.py b/tests/test_d9_evidence_bands.py new file mode 100644 index 00000000..b6384f9b --- /dev/null +++ b/tests/test_d9_evidence_bands.py @@ -0,0 +1,134 @@ +"""BUG-1318: D9 marriage rating is an evidence band, not an outcome claim. + +Upstream f891a2c1. The five bands must not say the relationship is stable, +ending, repeated, or in need of premarital counseling. The DK evidence line +keeps the 0-1 index and does not print a fake /100 score. +""" + +from __future__ import annotations + +import importlib.util +import json +import os +import re +import sys +from pathlib import Path + +ROOT = os.path.join(os.path.dirname(__file__), "..") +SCRIPTS = os.path.join(ROOT, "scripts") +if SCRIPTS not in sys.path: + sys.path.insert(0, SCRIPTS) + +from jyotish_engine import compute_chart_data # noqa: E402 + + +def _api_handler(): + """Reuse the existing unbound handler helper. Do not add another forgery site.""" + path = os.path.join(os.path.dirname(__file__), "test_api_server_security.py") + spec = importlib.util.spec_from_file_location("_api_security_helpers", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module._handler() +from marriage_counting import ( # noqa: E402 + D9_CLAIM_BOUNDARY, + D9_EVIDENCE_BANDS, + _assess_d9_marriage_quality, + marriage_counting_full_analysis, + render_d9_evidence_report, +) +from varga import calc_varga # noqa: E402 + +BANNED = re.compile(r"稳定|离婚|再婚|婚前咨询|几段") + + +def _quality(points_chart: dict) -> dict: + return _assess_d9_marriage_quality(points_chart, None) + + +def test_five_bands_have_no_outcome_claims_and_keep_the_boundary() -> None: + samples = { + "supportive_evidence_stronger": {"Venus": 331.0, "Mars": 271.0, "Jupiter": 91.0}, + "supportive_evidence_mixed_positive": {"Venus": 31.0}, + "mixed_evidence": {"Venus": 10.0}, + "pressure_evidence_mixed": {"Venus": 151.0}, + "pressure_evidence_stronger": {"Venus": 151.0, "Mars": 91.0, "Jupiter": 271.0}, + } + seen = [] + for key, chart in samples.items(): + quality = _quality(chart) + assert quality["quality_band"] == key + assert quality["claim_boundary"] == D9_CLAIM_BOUNDARY + assert quality["claim_boundary_en"] + assert not BANNED.search(quality["quality_rating"]) + assert not BANNED.search(quality["quality_rating_en"]) + seen.append(quality["quality_band"]) + assert seen == [band[0] for band in D9_EVIDENCE_BANDS] + + +def test_public_chart_report_snapshots_in_both_languages() -> None: + cases = json.loads( + Path(ROOT, "references/real_case_calibration/minute_rectification_development_v1.json").read_text(encoding="utf-8") + )["cases"] + birth = next(case for case in cases if case["case_id"] == "steve_jobs_1955_development")["birth"] + assert birth["source"]["rodden_rating"] == "AA" + year, month, day = (int(part) for part in birth["date"].split("-")) + hour, minute = (int(part) for part in birth["time"].split(":")) + chart, *_ = compute_chart_data( + year, month, day, hour, minute, + birth["latitude"], birth["longitude"], birth["timezone_offset"], + ayanamsa_name="raman", + ) + longitudes = {name: row["degree_raw"] for name, row in chart["planets"].items()} + d9 = {} + for name, lon in longitudes.items(): + divisional = calc_varga(lon, 9) + d9[name] = divisional["sign_idx"] * 30 + divisional["degree_in_sign"] + result = marriage_counting_full_analysis(chart["houses"]["house_7"]["lord"], longitudes, d9) + quality = result["d9_marriage_quality"] + assert quality["quality_band"] == "mixed_evidence" + assert quality["quality_points"] == 0 + chinese = render_d9_evidence_report(quality, "zh") + english = render_d9_evidence_report(quality, "en") + assert chinese == ( + "D9 证据档位:证据混杂\n" + "D9 结构证据指数,不预测婚姻次数、稳定、离婚、伴侣好坏,也不提示需要婚前咨询" + ) + assert english == ( + "D9 evidence band: Mixed evidence\n" + "D9 structural evidence index. It does not predict marriage count, stability, " + "divorce, or partner quality, and it does not suggest premarital counseling." + ) + assert "/100" not in chinese + assert "/100" not in english + + +def test_dk_evidence_line_prints_the_zero_to_one_index() -> None: + cases = json.loads( + Path(ROOT, "references/real_case_calibration/minute_rectification_development_v1.json").read_text(encoding="utf-8") + )["cases"] + birth = next(case for case in cases if case["case_id"] == "steve_jobs_1955_development")["birth"] + year, month, day = (int(part) for part in birth["date"].split("-")) + hour, minute = (int(part) for part in birth["time"].split(":")) + chart, asc_idx, *_ = compute_chart_data( + year, month, day, hour, minute, + birth["latitude"], birth["longitude"], birth["timezone_offset"], + ayanamsa_name="raman", + ) + longitudes = {name: row["degree_raw"] for name, row in chart["planets"].items()} + handler = _api_handler() + evidence = handler._compute_relationship_timing_evidence( + {}, + chart["planets"], + longitudes, + asc_idx, + chart["ascendant"]["sign"], + ) + notes = [row["note"] for row in evidence["evidence"] if row.get("label") == "Darakaraka (DK)"] + assert len(notes) == 1 + assert "/100" not in notes[0] + assert "婚姻质量指数" in notes[0] + assert "(0到1)" in notes[0] + score = evidence["darakaraka"]["marriage_quality_score"] + assert isinstance(score, float) + assert 0.0 <= score <= 1.0 + assert str(score) in notes[0]