From 4cdb9978003db471dca7cd144da68cb191e46488 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Wed, 14 Oct 2026 00:48:53 +0800 Subject: [PATCH] =?UTF-8?q?=E7=91=9C=E4=BC=BD=E8=A7=92=E5=AE=AB=E4=B8=BB?= =?UTF-8?q?=E5=92=8C=E4=B8=89=E6=96=B9=E4=B8=BB=E6=8C=89=E5=90=8D=E5=AD=97?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=EF=BC=8C=E4=B8=8D=E5=86=8D=E9=9A=8F=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E5=93=88=E5=B8=8C=E5=8F=98=EF=BC=88BUG-1320=EF=BC=8C?= =?UTF-8?q?=E4=B8=8A=E6=B8=B8=200337b77f=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同一张盘在两个哈希种子下,宫主名单和瑜伽参与者顺序一致。pytest tests/test_yoga_lord_order_stable.py 1 通过。 Co-Authored-By: Grok (agent loop) --- docs/BUG_HISTORY.md | 15 ++++++ scripts/yoga_engine.py | 6 ++- tests/test_yoga_lord_order_stable.py | 72 ++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/test_yoga_lord_order_stable.py diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index e16ccbe4..bd8daf5f 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -17750,3 +17750,18 @@ - 复发自:无。 - 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。 +## BUG-1320 | 瑜伽角宫主和三方主的顺序随进程哈希变化 + +- 状态:fixed-pending-verify +- 首次发现 / 最近更新:2026-10-14 / 2026-10-14 +- 来源:issue #56。上游 `0337b77f`。任务书 T3。 +- 现象:同一张盘的角宫主、三方主列表用了集合去重。集合顺序会跟着进程哈希变,瑜伽参与者顺序不稳定。 +- 触发条件:规则表达式读取这两份宫主列表。 +- 根因:去重用了不保证顺序的集合。同步第六轮漏了上游把它们改成排序的那一处。 +- 修复:两处都改成先去重再按名字排序。 +- 验证:`tests/test_yoga_lord_order_stable.py` 1 通过。哈希种子 0 和 1 两个进程算出的参与者顺序相同。 +- 防复发:两个子进程的名单必须逐项相同,并且等于排好序的名单。 +- 相关记录:BUG-1319。 +- 复发自:无。 +- 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。 + diff --git a/scripts/yoga_engine.py b/scripts/yoga_engine.py index 66cf5740..0f4db644 100644 --- a/scripts/yoga_engine.py +++ b/scripts/yoga_engine.py @@ -1687,11 +1687,13 @@ class YogaEngine: # 辅助:获取角宫主星列表 def _kendra_lords_list(): ai = SIGNS.index(ctx.ascendant) if ctx.ascendant in SIGNS else 0 - return list(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 4, 7, 10]])) + # set 顺序随进程哈希变化;排序后同一张盘的角宫主参与者顺序才稳定。 + return sorted(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 4, 7, 10]])) # 辅助:获取三方宫主星列表 def _trikona_lords_list(): ai = SIGNS.index(ctx.ascendant) if ctx.ascendant in SIGNS else 0 - return list(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 5, 9]])) + # set 顺序随进程哈希变化;排序后同一张盘的三方主参与者顺序才稳定。 + return sorted(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 5, 9]])) safe_globals = { # 安全内置函数:只开放规则表达式需要的纯函数,避免 custom 规则因 all/any/len/set 缺失而静默失效。 diff --git a/tests/test_yoga_lord_order_stable.py b/tests/test_yoga_lord_order_stable.py new file mode 100644 index 00000000..8bf3ad9b --- /dev/null +++ b/tests/test_yoga_lord_order_stable.py @@ -0,0 +1,72 @@ +"""BUG-1320: kendra and trikona lord lists do not follow PYTHONHASHSEED. + +Upstream 0337b77f. Two processes with different hash seeds must list the +same yoga participants in the same order. +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + +_PROBE = r""" +import json +import sys +sys.path.insert(0, "scripts") +from yoga_engine import YogaContext, YogaEngine, detect_yogas + +planets = { + "Sun": {"sign": "Leo", "house": 5, "degree": 10}, + "Moon": {"sign": "Cancer", "house": 4, "degree": 5}, + "Mars": {"sign": "Aries", "house": 1, "degree": 8}, + "Mercury": {"sign": "Virgo", "house": 6, "degree": 12}, + "Jupiter": {"sign": "Sagittarius", "house": 9, "degree": 15}, + "Venus": {"sign": "Libra", "house": 7, "degree": 3}, + "Saturn": {"sign": "Capricorn", "house": 10, "degree": 20}, + "Rahu": {"sign": "Taurus", "house": 2, "degree": 9}, + "Ketu": {"sign": "Scorpio", "house": 8, "degree": 9}, +} +rules = "references/yoga_rules.json" +engine = YogaEngine(rules) +ctx = YogaContext(planets, "Aries") +namespace = engine._build_custom_exec_globals(ctx, {}, {}) +rows = detect_yogas(planets, "Aries", rules) +print(json.dumps({ + "kendra": namespace["kendra_lords_list"](), + "trikona": namespace["trikona_lords_list"](), + "participants": [ + [row.get("rule_id"), str(row.get("combination"))] + for row in rows + ], +}, ensure_ascii=False)) +""" + + +def _run(seed: str) -> dict: + env = os.environ.copy() + env["PYTHONHASHSEED"] = seed + env["PYTHONIOENCODING"] = "utf-8" + completed = subprocess.run( + [sys.executable, "-c", _PROBE], + cwd=ROOT, + env=env, + check=True, + capture_output=True, + text=True, + encoding="utf-8", + ) + return json.loads(completed.stdout) + + +def test_yoga_participants_keep_the_same_order_across_hash_seeds() -> None: + first = _run("0") + second = _run("1") + assert first == second + assert first["kendra"] == sorted(first["kendra"]) + assert first["trikona"] == sorted(first["trikona"]) + assert first["kendra"] == ["Mars", "Moon", "Saturn", "Venus"] + assert first["trikona"] == ["Jupiter", "Mars", "Sun"]