refactor: context_builder 按 slice 拆到 src/llm/slices/ 包

单文件拆分(仅搬迁无逻辑修改):
- common.py    共享类型/头信息/_outcome/_is_stats_available
- form.py      form_slice + _get_form
- h2h.py       h2h_slice + _get_h2h
- stats.py     stats_slice(复用 form._get_form)
- home_away.py home_away_slice + _get_home_away
- standings.py standings_slice
- aggregate.py build_context

context_builder.py 改为纯 re-export 门面,公开签名不变。
同步修复测试 patch 目标(p0_home_away/h2h_perspective/multi_agent_cutoff)
与 regressions 源码断言(读 slices/*.py)。

全量测试 270 通过。
This commit is contained in:
shangfangjian
2026-09-22 01:33:14 +08:00
parent f1016b610a
commit 44816794d3
13 changed files with 677 additions and 563 deletions
+3 -3
View File
@@ -70,7 +70,7 @@ class TestH2HCurrentHomePerspective:
_make_h2h_match(101, home_id=2, away_id=1, home_goals=3, away_goals=1,
home_name="阿森纳", away_name="利物浦"),
]
import src.llm.context_builder as cb
import src.llm.slices.h2h as cb
orig = cb._get_h2h
async def mock_get_h2h(db, home_id, away_id, before, *, limit):
return matches
@@ -100,7 +100,7 @@ class TestH2HCurrentHomePerspective:
_make_h2h_match(201, home_id=1, away_id=2, home_goals=2, away_goals=1,
home_name="曼城", away_name="诺维奇"),
]
import src.llm.context_builder as cb
import src.llm.slices.h2h as cb
async def mock_get_h2h(db, h, a, before, **kw):
# 真实契约是 async(见 context_builder.py 的 `h2h = await _get_h2h(...)`),
@@ -122,7 +122,7 @@ class TestH2HCurrentHomePerspective:
_make_h2h_match(301, home_id=2, away_id=1, home_goals=0, away_goals=2,
home_name="热刺", away_name="切尔西"), # 切尔西客场 2-0 赢
]
import src.llm.context_builder as cb
import src.llm.slices.h2h as cb
orig = cb._get_h2h
async def mock_get_h2h(db, h, a, before, *, limit):
return matches
+1 -1
View File
@@ -206,7 +206,7 @@ class TestBacktestXgNotVisible:
header = _make_header(match_dt)
import src.llm.context_builder as cb
import src.llm.slices.stats as cb
async def mock_get_form(db, team_id, before, *, limit=10):
# before=cutoff(1月13日),比赛在1月15日,满足 before 条件
+3 -3
View File
@@ -101,7 +101,7 @@ class TestFormSliceHomeAwayIdentity:
home_name="曼城",
away_name="利物浦",
)
import src.llm.context_builder as cb
import src.llm.slices.form as cb
orig_get_form = cb._get_form
async def mock_get_form(db, team_id, before, *, limit):
return [hist_match] if team_id == 1 else []
@@ -132,7 +132,7 @@ class TestFormSliceHomeAwayIdentity:
home_name="阿森纳",
away_name="切尔西",
)
import src.llm.context_builder as cb
import src.llm.slices.form as cb
orig_get_form = cb._get_form
async def mock_get_form(db, team_id, before, *, limit):
return [hist_match] if team_id == 2 else []
@@ -169,7 +169,7 @@ class TestStatsSliceHomeAwayIdentity:
stats=_make_stats(home_xg=2.5, away_xg=0.8, home_shots=15, away_shots=5,
home_sot=6, away_sot=2, home_poss=60.0),
)
import src.llm.context_builder as cb
import src.llm.slices.stats as cb
orig_get_form = cb._get_form
async def mock_get_form(db, team_id, before, *, limit):
return [hist_match] if team_id == 1 else []
+16 -9
View File
@@ -32,17 +32,24 @@ class TestEagerLoadCoverage:
models.py 已声明 lazy="selectin" 兜底,但这里同时检查显式
selectinload —— 显式声明是查询意图的固化,也被 P0 修复所依赖。
(context_builder 已按 slice 拆分到 src/llm/slices/,getter 随实现迁移。)
"""
src = _read("llm/context_builder.py")
for rel in ("llm/slices/form.py", "llm/slices/h2h.py", "llm/slices/home_away.py"):
src = _read(rel)
for fn in ("_get_form", "_get_h2h", "_get_home_away"):
# 截取函数体(仅当前文件定义了该函数才检查)
m = re.search(rf"async def {fn}\(.*?(?=\nasync def |\n# =|\Z)", src, re.S)
if not m:
continue
body = m.group(0)
assert "selectinload" in body, (
f"{fn} 查询 Match 但未 eager-load 关系 —— "
"this would raise MissingGreenlet in async SQLAlchemy (P0-2)"
)
# 守卫完整性: 三个 getter 必须都能在 slices 包中找到
all_src = "\n".join(_read(r) for r in ("llm/slices/form.py", "llm/slices/h2h.py", "llm/slices/home_away.py"))
for fn in ("_get_form", "_get_h2h", "_get_home_away"):
# 截取函数体
m = re.search(rf"async def {fn}\(.*?(?=\nasync def |\n# =|\Z)", src, re.S)
assert m, f"{fn} 未找到"
body = m.group(0)
assert "selectinload" in body, (
f"{fn} 查询 Match 但未 eager-load 关系 —— "
"this would raise MissingGreenlet in async SQLAlchemy (P0-2)"
)
assert f"async def {fn}(" in all_src, f"{fn} 未在 slices 包中找到(拆分后迁移缺失?)"
def test_backtest_candidates_eager_load(self):
"""回测取历史比赛必须 eager-load(否则 session 关闭后访问关系必炸)。"""