refactor(cheap): 品类 rubric 挂点归一——TRPG 内联迁出 fixture、查找只剩单源一路 (W-GENRE 收敛)

机制迁移(rubric 挂点归一工单第①步):品类条目唯一数据源 = fixtures/genre-rubrics/<genre>.json
(items 键,经 load_genre_checklist 加载);cheap_verify.GENRE_CHECKLISTS 内联字典退役、
verify_richness 查找逻辑去掉「内联优先、fixture 兜底」两路,只剩 fixture 一路。

TRPG 5 条自内联**机器迁入** trpg.json(git show 基线 ast 提取直写,非手抄;逐字节比对
name/layer/meaning 全等)——meaning 内嵌「(正:…;反:…)」为内联时代原文原样保留,保 judge
prompt 逐字节不变;positive/negative 为机器拆出的同文抄录(金标锚材料,不进 prompt)。

锚定纪律机器验证(基线版模块独立加载对拍):品类路/无品类路 judge user prompt、两个 system
prompt 与 db4aadd7 基线**逐字节恒等**=迁移零评分尺漂移;金标复验随本工单后续步骤统一跑。

单测适配:4 处 GENRE_CHECKLISTS 引用改经 load_genre_checklist('trpg')(断言语义不降级),
trpg 条目名/顺序锁定断言补齐(同通用 8 条锁定),新增单源守门测试(GENRE_CHECKLISTS 不存在,
防两路复活)。test_cheap_verify 20 + test_genre_puzzle_assets 6 + test_golden_specs 6 = 32 绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-03 00:06:16 -07:00
parent db4aadd785
commit 5725f4683e
3 changed files with 64 additions and 25 deletions

View File

@ -50,20 +50,15 @@ _MAX = len(RICHNESS_CHECKLIST) # = 11
_LAYERS = ("L2", "L3", "L4") # 三分组固定顺序(消费面按分组小计各自取阈,见 §3.6 档位表)
# ── 品类扩展 rubric(W-GENRE 品类件④;质量模型 SoT §4 规范二)──
# 每品类另加 ≥4 条品类特有维度:同一字段形状 (标准名, 层标, 含义含一对正反例)、同样标层,
# 数据源唯一 = fixtures/genre-rubrics/<genre>.json(items 键,条目 {name,layer,meaning,positive,negative}),
# 经 load_genre_checklist 加载。2026-07-02 rubric 挂点归一:原内联 GENRE_CHECKLISTS 字典退役,
# TRPG 条目一字不动迁入 trpg.json——品类条目自此只有 fixture 一路,不再留内联第二源。
# 每品类另加 ≥4 条品类特有维度:同一字段形状 (标准名, 层标, 含义)、同样标层,
# **分母独立小计**(与通用底座分开、不混合平均——评分输出落 richness.genre.groups,通用 groups 不变)。
# 这仍是【喂给 LLM judge 的评分尺】数据、不是代码校验(红线:不进九门/不进 verdict/不进脚手架);
# 设计侧同源对齐 = .agents/skills/<genre>-game-design.md §10 品类自检。
# 锚定纪律(规范三):品类条目文本变更须金标复验(金标正例 + 薄反例,分组小计漂移 ≤±1 即回退)。
GENRE_CHECKLISTS = {
"trpg": [
("掷骰过程可见", "L2", "掷骰判定(骰值+加成 vs 难度)以飘字/动画亮给玩家,不是黑箱出结果(正:飘「d20:15+2 vs 12 ✓」;反:点卡直接加钱、不见骰)"),
("风险回报取舍", "L2", "同屏遭遇/选项的难度与奖励差异化亮牌,挑哪个的决定影响期望收益(正:骷髅卫 ≥9 +7金 vs 宝箱 ≥4 +3金;反:所有卡同难同赏点哪张都一样)"),
("成长进判定公式", "L2", "等级/伙伴等成长真实改变掷骰判定数值且 UI 可见,不是摆设数字(正:升级→骰+1 写面板、进公式;反:等级只是显示数字、判定恒裸 d20)"),
("难度爬坡登顶", "L3", "难度随层/进度递升,有明确通关终点与进度可见(正:第 N/6 层 HUD + 每层难度 +1 + 登顶 win;反:无限平难度刷分无终点)"),
("冒险战报炫耀", "L4", "结算屏讲这局冒险的故事(层数/击败/大成功/胜负因由),可截图分享(正:「爬到第 5 层·击败 7·大成功 2·勇者倒下」;反:只有一个总分数字)"),
],
}
_GENRE_RUBRICS_DIR = Path(__file__).resolve().parent / "fixtures" / "genre-rubrics"
# 品类脚手架目录名 → 品类 key(run_studio 据 scaffold_template 透传 genre;None/未登记 → 不评品类扩展)。
GENRE_BY_TEMPLATE = {
@ -71,13 +66,9 @@ GENRE_BY_TEMPLATE = {
"_template-puzzle": "puzzle",
}
# 品类条目第二来源 = fixtures/genre-rubrics/<genre>.json(数据件;解谜首用)。查找顺序 = 内联
# GENRE_CHECKLISTS 优先、fixture 兜底(两源归一是 rubric 挂点收敛工单的事,本处只保证两路都能生效)。
_GENRE_RUBRICS_DIR = Path(__file__).resolve().parent / "fixtures" / "genre-rubrics"
def load_genre_checklist(genre) -> list:
"""读品类 rubric fixture → [(标准名, 层标, 判据)*];genre 空/文件缺/坏条目 → [](绝不抛)。
"""读品类 rubric fixture(品类条目唯一数据源)→ [(标准名, 层标, 判据)*];genre 空/文件缺/坏条目 → [](绝不抛)。
条目形状随 fixture(name/layer/meaning,正反例 positive/negative 只作金标锚材料、不进 judge prompt
——prompt 里逐条只给一句可判定判据,与通用底座条目同形)。
@ -342,9 +333,9 @@ async def verify_richness(game_id: str, *, brief: str = "", model=None, sources=
sources: 可选直接注入的源码文本(单测用,绕过文件 I/O);None → 从 game_id 收集。
timeout: judge LLM 调用超时秒数;超时 → degraded(绝不卡死收口)。
max_src_bytes: 喂 judge 的源码上限。
genre: 品类 key(W-GENRE 件④);命中 GENRE_CHECKLISTS 或 fixtures/genre-rubrics/<genre>.json →
genre: 品类 key(W-GENRE 件④);命中 fixtures/genre-rubrics/<genre>.json(唯一数据源)→
同一次评分 additive 追加品类扩展条目(分母独立小计落 result["genre"]);
None/两处均未登记 → 行为与既有版本完全一致(prompt 逐字节不变)。
None/fixture 缺 → 行为与既有版本完全一致(prompt 逐字节不变)。
Returns:
成功 → parse_judge_output 的结构化评分(含 judgeTokens 观测,best-effort);
@ -362,9 +353,9 @@ async def verify_richness(game_id: str, *, brief: str = "", model=None, sources=
import _bootstrap # 代理旁路 + key 注入由 _bootstrap.build_cheap_model 内部处理
m = _bootstrap.build_cheap_model(max_tokens=_JUDGE_MAX_TOKENS)
# 品类扩展(内联 GENRE_CHECKLISTS 优先、fixtures/genre-rubrics/<genre>.json 兜底;
# 两处都没有 → 静默按 None——评分尺没有它的条目,别让评分半路造尺)。
genre_checklist = (GENRE_CHECKLISTS.get(genre) or load_genre_checklist(genre) or None) if genre else None
# 品类扩展(唯一数据源 = fixtures/genre-rubrics/<genre>.json,经 load_genre_checklist;
# 没有 fixture → 静默按 None——评分尺没有它的条目,别让评分半路造尺)。
genre_checklist = (load_genre_checklist(genre) or None) if genre else None
genre_key = genre if genre_checklist else None
sys_msg = SystemMsg(name="system", content=_JUDGE_SYSTEM_GENRE if genre_checklist else _JUDGE_SYSTEM)

View File

@ -0,0 +1,41 @@
{
"genre": "trpg",
"_note": "TRPG 品类丰富度 rubric 扩展(W-GENRE 品类件④·机器版)。喂 cheap_verify 的 LLM 验证 agent:与通用底座 11 条同一次评分调用、同一 0/1+层标形状,但分母独立小计(质量模型 SoT §3.3 规范二,不与底座混合平均);纯 LLM 非阻塞,绝不写成代码校验/不进九门/不进脚手架。2026-07-02 rubric 挂点归一:条目自 cheap_verify.GENRE_CHECKLISTS['trpg'](caadfe4c 内联)机器迁入本文件、一字不动——meaning 内嵌「(正:…;反:…)」是内联时代原文,为锚定纪律(judge prompt 逐字节不变)原样保留;positive/negative 字段为自 meaning 机器拆出的同文抄录(金标锚材料,不进 judge prompt)。与 .agents/skills/trpg-game-design.md §10 品类自检同源——改任一处必须同改另一处,且评分尺变更须金标复验(单款分组小计漂移 >±1 即回退,规范三)。金标锚:正例=_template-trpg 骨架(merge 前基线:品类 5/5、通用 8/11),薄反例=合成黑箱点卡(点卡直接加钱、无骰无取舍无成长无终点;merge 前基线:品类 0/5、通用 0/11)。",
"items": [
{
"name": "掷骰过程可见",
"layer": "L2",
"meaning": "掷骰判定(骰值+加成 vs 难度)以飘字/动画亮给玩家,不是黑箱出结果(正:飘「d20:15+2 vs 12 ✓」;反:点卡直接加钱、不见骰)",
"positive": "飘「d20:15+2 vs 12 ✓」",
"negative": "点卡直接加钱、不见骰"
},
{
"name": "风险回报取舍",
"layer": "L2",
"meaning": "同屏遭遇/选项的难度与奖励差异化亮牌,挑哪个的决定影响期望收益(正:骷髅卫 ≥9 +7金 vs 宝箱 ≥4 +3金;反:所有卡同难同赏点哪张都一样)",
"positive": "骷髅卫 ≥9 +7金 vs 宝箱 ≥4 +3金",
"negative": "所有卡同难同赏点哪张都一样"
},
{
"name": "成长进判定公式",
"layer": "L2",
"meaning": "等级/伙伴等成长真实改变掷骰判定数值且 UI 可见,不是摆设数字(正:升级→骰+1 写面板、进公式;反:等级只是显示数字、判定恒裸 d20)",
"positive": "升级→骰+1 写面板、进公式",
"negative": "等级只是显示数字、判定恒裸 d20"
},
{
"name": "难度爬坡登顶",
"layer": "L3",
"meaning": "难度随层/进度递升,有明确通关终点与进度可见(正:第 N/6 层 HUD + 每层难度 +1 + 登顶 win;反:无限平难度刷分无终点)",
"positive": "第 N/6 层 HUD + 每层难度 +1 + 登顶 win",
"negative": "无限平难度刷分无终点"
},
{
"name": "冒险战报炫耀",
"layer": "L4",
"meaning": "结算屏讲这局冒险的故事(层数/击败/大成功/胜负因由),可截图分享(正:「爬到第 5 层·击败 7·大成功 2·勇者倒下」;反:只有一个总分数字)",
"positive": "「爬到第 5 层·击败 7·大成功 2·勇者倒下」",
"negative": "只有一个总分数字"
}
]
}

View File

@ -191,19 +191,26 @@ def test_verify_empty_sources_skips_llm():
# ── W-GENRE 品类件④:品类扩展 rubric(分母独立小计)────────────────────────────
def test_genre_checklist_trpg_shape():
"""trpg 品类扩展 ≥4 条、形状同通用底座 (名, 层标, 含义含正反例)、层标合法(§4 规范二)。"""
gc = cheap_verify.GENRE_CHECKLISTS["trpg"]
"""trpg 品类扩展(fixture 单源)≥4 条、形状同通用底座 (名, 层标, 含义含正反例)、层标合法(§4 规范二)。"""
gc = cheap_verify.load_genre_checklist("trpg")
assert len(gc) >= 4, f"品类扩展应 ≥4 条,得 {len(gc)}"
for name, layer, meaning in gc:
assert isinstance(name, str) and name, "标准名非空"
assert layer in ("L2", "L3", "L4"), f"层标非法:{layer}"
assert "正:" in meaning and "反:" in meaning, f"「{name}」含义应带一对正反例(规范三锚定)"
# 条目名/顺序锁定 = 内联时代原文(caadfe4c);迁出 fixture 后锚定不动(锚定纪律,同通用 8 条锁定)
assert [n for n, _l, _m in gc] == ["掷骰过程可见", "风险回报取舍", "成长进判定公式", "难度爬坡登顶", "冒险战报炫耀"]
assert cheap_verify.GENRE_BY_TEMPLATE["_template-trpg"] == "trpg"
def test_inline_genre_checklists_retired():
"""rubric 挂点归一不变量:内联 GENRE_CHECKLISTS 已退役,品类条目只剩 fixtures/genre-rubrics/ 一路(防两路复活)。"""
assert not hasattr(cheap_verify, "GENRE_CHECKLISTS"), "内联品类字典应已退役(单一数据源=fixture)"
def test_parse_with_genre_checklist_independent_denominator():
"""品类段独立分母:通用 score/groups 不被品类条目污染;richness.genre 形状齐全。"""
gc = cheap_verify.GENRE_CHECKLISTS["trpg"]
gc = cheap_verify.load_genre_checklist("trpg")
checks = [{"name": n, "hit": True, "why": "w"} for (n, _l, _m) in cheap_verify.RICHNESS_CHECKLIST]
checks += [{"name": n, "hit": (i != len(gc) - 1), "why": "w"} for i, (n, _l, _m) in enumerate(gc)]
r = cheap_verify.parse_judge_output(json.dumps({"checks": checks, "notes": "n"}),
@ -217,7 +224,7 @@ def test_parse_with_genre_checklist_independent_denominator():
def test_parse_genre_positional_fallback_offset():
"""品类段位置兜底:name 全对不上时按「通用在前、品类紧随」的偏移对位。"""
gc = cheap_verify.GENRE_CHECKLISTS["trpg"]
gc = cheap_verify.load_genre_checklist("trpg")
n = 11 + len(gc)
checks = [{"name": f"未知{i}", "hit": True, "why": ""} for i in range(n)]
r = cheap_verify.parse_judge_output(json.dumps({"checks": checks}),
@ -235,7 +242,7 @@ def test_parse_no_genre_backcompat_no_genre_key():
def test_build_judge_user_genre_block_additive():
"""user prompt:无品类段落基线不变;有品类只 additive 加品类清单段 + 条数契约(11+N)。"""
base = cheap_verify._build_judge_user("SRC", "B")
gc = cheap_verify.GENRE_CHECKLISTS["trpg"]
gc = cheap_verify.load_genre_checklist("trpg")
withg = cheap_verify._build_judge_user("SRC", "B", genre_key="trpg", genre_checklist=gc)
assert "品类扩展清单" not in base and "共 11 条" in base
assert "trpg 品类扩展清单" in withg and f"共 {11 + len(gc)} 条" in withg