瑜伽角宫主和三方主按名字排序,不再随进程哈希变(BUG-1320,上游 0337b77f)

同一张盘在两个哈希种子下,宫主名单和瑜伽参与者顺序一致。pytest tests/test_yoga_lord_order_stable.py 1 通过。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-14 00:48:53 +08:00
co-authored by Grok (agent loop)
parent 2748e43871
commit 4cdb997800
3 changed files with 91 additions and 2 deletions
+15
View File
@@ -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。
+4 -2
View File
@@ -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 缺失而静默失效。
+72
View File
@@ -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"]