feat(cheap): rubric v2 评分尺 8→11 条 + L2/L3/L4 三分组小计落库(质量模型 §3.3 计分制·前置小工单)
评分尺升级:通用底座 v2 = 现行 8 条(判据文本/顺序锁定、仅补层标)+ 新增 3 条(首3分钟脚本 L3 / 炫耀时刻 L4 / 同款钩子 L4)。逐条 0/1 标层,一次 LLM 评分产 L2×6/L3×3/L4×2 三分组小计;不设单一总分(§3.3 防 Goodhart)。 落库:summary.richness 从单一总分改为三分组小计——新增 groups 字段 + hits 每条带 layer;score 保留为向后兼容字段(供 bake_off.richness_dist 读、非设计总分)。两落库点(cheap_studio/cheap_service_driver)整体赋值自动继承新形状,仅日志 additive 补 groups。 红线:rubric 仍纯 LLM 非阻塞,不进 verdict/九门/达标判定;bake_off.py richness 正交红线一行未动。 验收(本机真跑 M3):金标复验 base1/base4/cheap-run-sc 原 8 条命中数漂移 = -1/0/0(≤±1,base1 唯一 -1 来自「30秒爽点」单条 LLM 边界抖动、非评分尺扰动);落库佐证 run-summary 真出现 richness.groups + hits.layer、既有键不受影响;test_cheap_verify 14 绿(+3 新测)、bake_off/service_driver 27 绿;全套另 14 failed 经 stash baseline 对比证实为 worktree 缺产物/node_modules 的 pre-existing 环境失败、零回归。 follow-up:bake_off.richness_dist 的 max=8 报告口径随 W-GENRE 品类批产接入 richnessByGenre 时对齐分组小计;sim-business skill §10 改口归质量设计档收口。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4d220e77da
commit
ba82e63c82
@ -314,7 +314,7 @@ async def drive_cheap_generation(job: dict, *, base_url: str | None = None, user
|
||||
summary["richness"] = await cheap_verify.verify_richness(game_id, brief=brief)
|
||||
rv = summary["richness"]
|
||||
print(f"[cheap-driver] game={game_id} 丰富度 score={rv.get('score')}/{rv.get('max')} "
|
||||
f"degraded={rv.get('degraded')}", flush=True)
|
||||
f"groups={rv.get('groups')} degraded={rv.get('degraded')}", flush=True)
|
||||
except Exception as e: # noqa: BLE001 —— richness 非阻塞铁律:绝不影响回调
|
||||
summary["richness"] = {"score": None, "degraded": True, "reason": f"richness 接线异常:{type(e).__name__}: {e}"}
|
||||
|
||||
|
||||
@ -287,7 +287,7 @@ async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=
|
||||
try:
|
||||
summary["richness"] = await cheap_verify.verify_richness(game_id, brief=brief)
|
||||
rv = summary["richness"]
|
||||
_rec(f"丰富度 LLM 评分 score={rv.get('score')}/{rv.get('max')} degraded={rv.get('degraded')}")
|
||||
_rec(f"丰富度 LLM 评分 score={rv.get('score')}/{rv.get('max')} groups={rv.get('groups')} degraded={rv.get('degraded')}")
|
||||
except Exception as e: # noqa: BLE001 非阻塞铁律:richness 绝不影响生成主链
|
||||
summary["richness"] = {"score": None, "degraded": True,
|
||||
"reason": f"richness 接线异常:{type(e).__name__}: {e}"}
|
||||
|
||||
@ -6,7 +6,8 @@ cheap_verify.py — 便宜档「丰富度」LLM 验证 agent(非阻塞·只报
|
||||
故本模块只做两件 100% 机械确定的事,「丰富不丰富」的判断本身完全交 LLM judge:
|
||||
① 读产物 L3 源码(确定性文件 I/O)+ 拼 judge prompt(确定性字符串);
|
||||
② 解析 LLM 返回的 JSON 成结构化评分(确定性解析 + degraded 兜底)。
|
||||
本模块**不含任何丰富度的代码断言**——8 条好玩清单只是【喂给 LLM 的评分尺】,命中与否全由 judge 读码后裁。
|
||||
本模块**不含任何丰富度的代码断言**——11 条丰富度清单只是【喂给 LLM 的评分尺】,命中与否全由 judge 读码后裁;
|
||||
评分按层标 L2/L3/L4 聚合成三分组小计(L2 内容丰富 / L3 留存结构 / L4 传播钩子,不设单一总分),规范见质量模型 SoT §3.3 计分制。
|
||||
(注:sim-business-game-design.md §10 写了「4 条可机检 code-presence」子集,那与本红线冲突——本模块刻意不照它写代码校验。)
|
||||
|
||||
非阻塞铁律:LLM 调失败/超时/解析失败 → 返回 {score:None, degraded:True, reason:...},
|
||||
@ -19,41 +20,78 @@ import asyncio
|
||||
|
||||
import cheap_run # 仅 stdlib 依赖(读产物 src 文件);不 import agentscope/_bootstrap,故纯函数可在无 agentscope 环境单测。
|
||||
|
||||
# ── sim-business §10 反「无趣」8 条好玩清单(judge 的评分 rubric)──
|
||||
# ── 通用底座 v2 = 11 条丰富度清单(judge 的评分 rubric)──
|
||||
# 这是【喂给 LLM judge 的打分尺】,不是代码校验:丰富不丰富全由 judge 读源码后逐条裁。
|
||||
# 与 .agents/skills/sim-business-game-design.md §10 同源(那边是 design 自检,这里是产物 judge 评分)。
|
||||
# 每项 = (标准名, 含义);judge 按标准名逐条回命中与否 + 理由。
|
||||
# 规范来源 = 质量模型 SoT《游戏质量与爆火能力·设计》§3.3 计分制(每条 0/1 + 层标 L2/L3/L4,产三分组小计、不设单一总分)。
|
||||
# v2 = 现行 8 条(与 .agents/skills/sim-business-game-design.md §10 同源)+ 新增 3 条(首 3 分钟脚本 / 炫耀时刻 / 同款钩子)。
|
||||
# 每项 = (标准名, 层标, 含义);judge 按标准名逐条回命中与否 + 理由,命中数按层聚合成三分组小计。
|
||||
# ★锚定纪律(§3.3 第 3 条):原 8 条判据文本与顺序一字不改(评分尺变更须金标复验、原 8 条判定漂移 ≤±1),
|
||||
# 新增 3 条追加于末尾;层标只是聚合归属,不改原条目的判据本身。
|
||||
# 分层含义:L2 内容丰富(有料耐玩)/ L3 留存结构(想再玩的结构前提)/ L4 传播钩子(想传出去的结构前提)。
|
||||
RICHNESS_CHECKLIST = [
|
||||
("即时反馈", "每个主要操作有「飘字 +N + 音效 + 粒子」即时回报"),
|
||||
("可见成长", "有数字/规模肉眼可见地变大(分数飙升/店铺扩张/等级上涨)"),
|
||||
("下一个解锁", "任意时刻玩家眼前都有「再攒一点就解锁 X」的钩子"),
|
||||
("30秒爽点", "开局 30 秒内有第一次升级/解锁"),
|
||||
("数值滚雪球", "成长有「越来越快」的暴富段,而非平淡线性"),
|
||||
("情感锚", "有萌角色/拥有物让玩家「想养大它」"),
|
||||
("放置回归", "离线回来有惊喜(离线收益弹窗等)"),
|
||||
("音反馈", "收益/升级/解锁有声音(哪怕程序化)"),
|
||||
# 原 8 条(文本/顺序锁定,仅补层标)
|
||||
("即时反馈", "L2", "每个主要操作有「飘字 +N + 音效 + 粒子」即时回报"),
|
||||
("可见成长", "L2", "有数字/规模肉眼可见地变大(分数飙升/店铺扩张/等级上涨)"),
|
||||
("下一个解锁", "L3", "任意时刻玩家眼前都有「再攒一点就解锁 X」的钩子"),
|
||||
("30秒爽点", "L2", "开局 30 秒内有第一次升级/解锁"),
|
||||
("数值滚雪球", "L2", "成长有「越来越快」的暴富段,而非平淡线性"),
|
||||
("情感锚", "L2", "有萌角色/拥有物让玩家「想养大它」"),
|
||||
("放置回归", "L3", "离线回来有惊喜(离线收益弹窗等)"),
|
||||
("音反馈", "L2", "收益/升级/解锁有声音(哪怕程序化)"),
|
||||
# 新增 3 条(v2 草案,随首个品类件落地验证;判据文本源自 §3.3 第 1 条)
|
||||
("首3分钟脚本", "L3", "首 3 分钟脚本成立:0–10s 零阅读上手 / 10–60s 首次升级 / 1–3min 露出 2–3 个后续锁"),
|
||||
("炫耀时刻", "L4", "有可截图分享的结算/成就画面(打完有结算展示场景,不是直接黑屏)"),
|
||||
("同款钩子", "L4", "有清晰的品类原型 + 主题标签元数据,可供 remix 预填做同款"),
|
||||
]
|
||||
|
||||
_MAX = len(RICHNESS_CHECKLIST) # = 8
|
||||
_MAX = len(RICHNESS_CHECKLIST) # = 11
|
||||
_LAYERS = ("L2", "L3", "L4") # 三分组固定顺序(消费面按分组小计各自取阈,见 §3.6 档位表)
|
||||
|
||||
|
||||
def _layer_maxes(checklist=RICHNESS_CHECKLIST) -> dict:
|
||||
"""各层条目数(分母)。通用底座 v2 = L2:6 / L3:3 / L4:2。"""
|
||||
m = {ly: 0 for ly in _LAYERS}
|
||||
for _name, layer, _meaning in checklist:
|
||||
m[layer] = m.get(layer, 0) + 1
|
||||
return m
|
||||
|
||||
|
||||
def _group_subtotals(hits: list, checklist=RICHNESS_CHECKLIST) -> dict:
|
||||
"""把逐条命中按层标聚合成三分组小计 {L2:{score,max}, L3:..., L4:...}。
|
||||
|
||||
这是 §3.3 计分制的落库主形状——三层各自对得上自己的消费点(品类件验收/批次观测/回流分析),
|
||||
**不做单一总分平均**(单一总分诱导 Goodhart,见 §3.3 第 1 条)。
|
||||
"""
|
||||
maxes = _layer_maxes(checklist)
|
||||
scores = {ly: 0 for ly in _LAYERS}
|
||||
for h in hits:
|
||||
ly = h.get("layer")
|
||||
if h.get("hit") and ly in scores:
|
||||
scores[ly] += 1
|
||||
return {ly: {"score": scores[ly], "max": maxes.get(ly, 0)} for ly in _LAYERS}
|
||||
|
||||
# 只读 L3 游戏本体(玩法/画面/数值/资产);跳过 L1 plumbing(host-config/game.js/index.html——
|
||||
# 固定脚手架、与「游戏丰富不丰富」无关,喂给 judge 只会稀释信号)。
|
||||
_L3_SOURCE_FILES = ("game-logic.js", "core.js", "render.js", "balance.js", "assets.js")
|
||||
_MAX_SRC_BYTES = 24000 # 喂 judge 的源码总量上限(控 token / 成本;超限按字节截断)。
|
||||
_JUDGE_MAX_TOKENS = 3000 # judge 输出小(8 条短理由 + 一句点评),3000 足够、M3 无 thinking。
|
||||
_JUDGE_MAX_TOKENS = 4000 # judge 输出小(11 条短理由 + 一句点评),4000 足够、M3 无 thinking。
|
||||
|
||||
# judge 的 system / user 框定——只评分、不改码、要看源码真实现、输出严格 JSON。
|
||||
_JUDGE_SYSTEM = (
|
||||
"你是轻量小游戏的【丰富度评审 agent】。任务 = 读一款便宜档 LittleJS 小游戏的源码,"
|
||||
"只评判它「作为一款游戏够不够丰富、好玩」,逐条给出 8 条好玩清单的命中与否 + 一句中文理由,"
|
||||
"只评判它「作为一款游戏够不够丰富、好玩、耐玩、想传出去」,逐条给出 11 条丰富度清单的命中与否 + 一句中文理由,"
|
||||
"最后输出严格 JSON。你只评分、不修改代码、不阻断发布——这是非阻塞的质量信号。"
|
||||
"评判要看源码里**真实实现**的玩法、数值成长、解锁阶梯、即时反馈与音效,别被空壳或注释骗。"
|
||||
"这 11 条分三层:L2 内容丰富(有料耐玩)、L3 留存结构(想再玩的结构前提)、L4 传播钩子(想让别人看/做同款的前提);"
|
||||
"评判要看源码里**真实实现**的玩法、数值成长、解锁阶梯、即时反馈、音效、结算画面与品类元数据,别被空壳或注释骗。"
|
||||
)
|
||||
|
||||
|
||||
def _degraded(reason: str) -> dict:
|
||||
"""降级结果(非阻塞铁律):score=None + degraded=True;score 为 None 表示「这次没评出来」,绝不参与达标。"""
|
||||
return {"score": None, "max": _MAX, "hits": [], "notes": None, "degraded": True, "reason": reason}
|
||||
"""降级结果(非阻塞铁律):score=None + degraded=True;score 为 None 表示「这次没评出来」,绝不参与达标。
|
||||
|
||||
groups 同置 None:三分组小计的消费面(品类件验收/回流分析)遇 None 即「本次未评出」,与 score=None 同义。
|
||||
"""
|
||||
return {"score": None, "max": _MAX, "hits": [], "groups": None, "notes": None, "degraded": True, "reason": reason}
|
||||
|
||||
|
||||
def _coerce_bool(v) -> bool:
|
||||
@ -70,11 +108,17 @@ def _coerce_bool(v) -> bool:
|
||||
def parse_judge_output(text, *, checklist=RICHNESS_CHECKLIST) -> dict:
|
||||
"""纯函数:把 judge 的 LLM 文本输出解析成结构化丰富度评分(含 degraded 兜底)。可单测、零网络、零 agentscope。
|
||||
|
||||
成功 → {score:int(命中数 0..8), max:8, hits:[{name,hit,why}*8], notes:str, degraded:False}
|
||||
解析失败(空/非 JSON 对象/缺 checks 数组)→ _degraded(...)(score=None, degraded=True)。
|
||||
成功 → {
|
||||
score:int(11 条命中总数 0..11), # ★向后兼容字段:仅供 bake_off.richness_dist 读;非「质量总分」。
|
||||
max:11, # 设计不设单一总分(诱导 Goodhart),主消费形状 = groups 三分组小计。
|
||||
hits:[{name,layer,hit,why}*11], # 逐条:加 layer 层标(L2/L3/L4)
|
||||
groups:{L2:{score,max}, L3:..., L4:...}, # ★三分组小计(§3.3 计分制主形状)
|
||||
notes:str, degraded:False}
|
||||
解析失败(空/非 JSON 对象/缺 checks 数组)→ _degraded(...)(score=None, groups=None, degraded=True)。
|
||||
|
||||
对齐策略:优先按 name 对齐 judge 的 checks 到 8 条标准项;name 对不上的按位置兜底(容忍模型改名/英文名)。
|
||||
本函数**不含任何丰富度的代码判断**——只搬运 judge 的逐条裁决并计命中数。
|
||||
对齐策略:优先按 name 对齐 judge 的 checks 到 11 条标准项;name 对不上的按位置兜底(容忍模型改名/英文名)。
|
||||
layer 层标取自 checklist(不依赖 LLM 回传),故分组小计对模型改名鲁棒。
|
||||
本函数**不含任何丰富度的代码判断**——只搬运 judge 的逐条裁决并按层聚合命中数。
|
||||
"""
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return _degraded("judge 输出为空")
|
||||
@ -99,17 +143,18 @@ def parse_judge_output(text, *, checklist=RICHNESS_CHECKLIST) -> dict:
|
||||
|
||||
hits = []
|
||||
score = 0
|
||||
for i, (name, _meaning) in enumerate(checklist):
|
||||
for i, (name, layer, _meaning) in enumerate(checklist):
|
||||
c = by_name.get(name)
|
||||
if c is None and i < len(checks_raw) and isinstance(checks_raw[i], dict):
|
||||
c = checks_raw[i] # name 对不上 → 位置兜底
|
||||
hit = _coerce_bool(c.get("hit")) if isinstance(c, dict) else False
|
||||
why = (str(c.get("why", "")).strip()[:200]) if isinstance(c, dict) else ""
|
||||
hits.append({"name": name, "hit": hit, "why": why})
|
||||
hits.append({"name": name, "layer": layer, "hit": hit, "why": why}) # layer 取自 checklist(权威)
|
||||
if hit:
|
||||
score += 1
|
||||
notes = str(data.get("notes", "")).strip()[:500]
|
||||
return {"score": score, "max": len(checklist), "hits": hits, "notes": notes, "degraded": False}
|
||||
return {"score": score, "max": len(checklist), "hits": hits,
|
||||
"groups": _group_subtotals(hits, checklist), "notes": notes, "degraded": False}
|
||||
|
||||
|
||||
def _collect_sources(game_id: str, *, max_bytes: int = _MAX_SRC_BYTES) -> str:
|
||||
@ -141,23 +186,24 @@ def _collect_sources(game_id: str, *, max_bytes: int = _MAX_SRC_BYTES) -> str:
|
||||
|
||||
|
||||
def _build_judge_user(src_text: str, brief: str = "") -> str:
|
||||
"""拼 judge 的 user 消息:brief(可选)+ 8 条清单 + 产物源码 + 严格 JSON 输出契约(纯确定性字符串拼接)。"""
|
||||
"""拼 judge 的 user 消息:brief(可选)+ 11 条清单(带层标)+ 产物源码 + 严格 JSON 输出契约(纯确定性字符串拼接)。"""
|
||||
checklist_lines = "\n".join(
|
||||
f"{i + 1}. {name}:{meaning}" for i, (name, meaning) in enumerate(RICHNESS_CHECKLIST)
|
||||
f"{i + 1}. [{layer}] {name}:{meaning}" for i, (name, layer, meaning) in enumerate(RICHNESS_CHECKLIST)
|
||||
)
|
||||
brief_block = f"这款游戏的 brief(玩家想要的):{brief}\n\n" if brief else ""
|
||||
return (
|
||||
f"{brief_block}"
|
||||
"下面是这款便宜档小游戏的 L3 源码。请逐条评判它是否命中这 8 条好玩清单,"
|
||||
"据**源码里真实实现了的玩法/数值/音效/解锁**判断(别被注释或空壳骗:比如只 import 了 audioMusic "
|
||||
"下面是这款便宜档小游戏的 L3 源码。请逐条评判它是否命中这 11 条丰富度清单(每条前的 [L2]/[L3]/[L4] 是分层标注——"
|
||||
"L2 内容丰富、L3 留存结构、L4 传播钩子——仅用于分组,不改变你对每条的独立判命中),"
|
||||
"据**源码里真实实现了的玩法/数值/音效/解锁/结算画面/品类元数据**判断(别被注释或空壳骗:比如只 import 了 audioMusic "
|
||||
"但收益处没真调 playSfx,则「音反馈」不算命中):\n\n"
|
||||
f"{checklist_lines}\n\n"
|
||||
"=== 源码开始 ===\n"
|
||||
f"{src_text}\n"
|
||||
"=== 源码结束 ===\n\n"
|
||||
"严格只输出以下 JSON(不要任何额外文字、不要 markdown 围栏):\n"
|
||||
'{"checks":[{"name":"即时反馈","hit":true,"why":"一句中文理由"}, … 共 8 条,'
|
||||
'name 用上面 8 条的中文名、hit 为 true/false], "notes":"整体一句话点评"}'
|
||||
'{"checks":[{"name":"即时反馈","hit":true,"why":"一句中文理由"}, … 共 11 条,'
|
||||
'name 用上面 11 条的中文名、hit 为 true/false], "notes":"整体一句话点评"}'
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
"""
|
||||
test_cheap_verify.py — U-A2 便宜档「丰富度」LLM 验证 agent 单测。
|
||||
test_cheap_verify.py — U-A2 便宜档「丰富度」LLM 验证 agent 单测(rubric v2:11 条 + 三分组小计)。
|
||||
|
||||
两层:
|
||||
① parse_judge_output 纯函数(解析 LLM 输出 → 结构化评分 + degraded 兜底)——无 agentscope、零网络,
|
||||
① parse_judge_output 纯函数(解析 LLM 输出 → 结构化评分 + 三分组小计 + degraded 兜底)——无 agentscope、零网络,
|
||||
plain python 即可跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_cheap_verify.py
|
||||
② verify_richness 非阻塞契约(fake model 注入,零真网络)——验证「LLM 失败/超时/解析失败/空 src → degraded、
|
||||
绝不抛、绝不阻断」。需 agentscope.message(venv 有),缺则自动跳过。
|
||||
|
||||
rubric v2(质量模型 SoT §3.3):通用底座 11 条,分层 L2×6 / L3×3 / L4×2;
|
||||
逐条 0/1 标层 + L2/L3/L4 三分组小计;score 保留为向后兼容字段(bake_off 读),非单一总分。
|
||||
|
||||
跑:cheap-worker/.venv/bin/python -m pytest cheap-worker/tests/test_cheap_verify.py -q
|
||||
"""
|
||||
|
||||
@ -26,35 +29,66 @@ except Exception: # noqa: BLE001 无 agentscope 时 ② 类测试跳过(纯
|
||||
|
||||
|
||||
def _full_checks(hits):
|
||||
"""据布尔列表造 8 条 checks(name 用 cheap_verify 的标准名)。"""
|
||||
return [{"name": n, "hit": h, "why": "理由"} for (n, _m), h in zip(cheap_verify.RICHNESS_CHECKLIST, hits)]
|
||||
"""据布尔列表造 checks(name 用 cheap_verify 的标准名);hits 短于 11 条则 zip 取最短(余条判 False)。"""
|
||||
return [{"name": n, "hit": h, "why": "理由"} for (n, _l, _m), h in zip(cheap_verify.RICHNESS_CHECKLIST, hits)]
|
||||
|
||||
|
||||
# 通用底座 v2 层索引(据 RICHNESS_CHECKLIST 顺序):L2={0,1,3,4,5,7} L3={2,6,8} L4={9,10}
|
||||
def test_checklist_shape_v2():
|
||||
"""底座 = 11 条,分层 L2×6 / L3×3 / L4×2(分组归属对齐 §3.3 第 1 条)。"""
|
||||
cl = cheap_verify.RICHNESS_CHECKLIST
|
||||
assert len(cl) == 11 and cheap_verify._MAX == 11
|
||||
layers = [layer for _n, layer, _m in cl]
|
||||
assert layers.count("L2") == 6 and layers.count("L3") == 3 and layers.count("L4") == 2
|
||||
# 原 8 条 name/顺序锁定(锚定纪律:评分尺变更不得动原条目)
|
||||
assert [n for n, _l, _m in cl][:8] == [
|
||||
"即时反馈", "可见成长", "下一个解锁", "30秒爽点", "数值滚雪球", "情感锚", "放置回归", "音反馈"]
|
||||
|
||||
|
||||
# ───────────────────────── ① parse_judge_output 纯函数(无 agentscope/无网络)─────────────────────────
|
||||
|
||||
def test_parse_valid_full():
|
||||
"""合法 8 条 JSON → 命中数正确、非 degraded、hits/notes 落位。"""
|
||||
"""合法 JSON(喂前 8 条命中态)→ 命中数正确、非 degraded、hits 带 layer、groups 三分组小计落位。"""
|
||||
text = json.dumps({"checks": _full_checks([True, True, False, True, False, False, False, True]),
|
||||
"notes": "还行"}, ensure_ascii=False)
|
||||
r = cheap_verify.parse_judge_output(text)
|
||||
assert r["degraded"] is False
|
||||
assert r["score"] == 4 and r["max"] == 8
|
||||
assert len(r["hits"]) == 8 and r["hits"][0]["name"] == "即时反馈"
|
||||
assert r["score"] == 4 and r["max"] == 11
|
||||
assert len(r["hits"]) == 11 and r["hits"][0]["name"] == "即时反馈" and r["hits"][0]["layer"] == "L2"
|
||||
# 前 8 条命中态 [T,T,F,T,F,F,F,T]:L2(0,1,3,4,5,7)=4;L3(2,6,8=F,F,未喂F)=0;L4(9,10 未喂)=0
|
||||
assert r["groups"] == {"L2": {"score": 4, "max": 6}, "L3": {"score": 0, "max": 3}, "L4": {"score": 0, "max": 2}}
|
||||
assert r["notes"] == "还行"
|
||||
|
||||
|
||||
def test_parse_11_full_and_groups():
|
||||
"""11 条全命中 → score=11、三分组小计满分(L2 6/6 · L3 3/3 · L4 2/2)。"""
|
||||
r = cheap_verify.parse_judge_output(json.dumps({"checks": _full_checks([True] * 11), "notes": "满"}))
|
||||
assert r["degraded"] is False and r["score"] == 11 and r["max"] == 11
|
||||
assert r["groups"] == {"L2": {"score": 6, "max": 6}, "L3": {"score": 3, "max": 3}, "L4": {"score": 2, "max": 2}}
|
||||
|
||||
|
||||
def test_parse_groups_by_layer():
|
||||
"""按层聚合正确:命中态 [T,T,F,T,F,F,F,T,F,T,T] → L2=4 L3=0 L4=2、score=6。"""
|
||||
pat = [True, True, False, True, False, False, False, True, False, True, True]
|
||||
r = cheap_verify.parse_judge_output(json.dumps({"checks": _full_checks(pat)}))
|
||||
assert r["score"] == 6
|
||||
assert r["groups"]["L2"] == {"score": 4, "max": 6}
|
||||
assert r["groups"]["L3"] == {"score": 0, "max": 3}
|
||||
assert r["groups"]["L4"] == {"score": 2, "max": 2}
|
||||
|
||||
|
||||
def test_parse_fenced_json():
|
||||
"""markdown 围栏 + 前后赘语 → json_repair 兜得住、正确解析。"""
|
||||
body = json.dumps({"checks": _full_checks([True] * 8), "notes": "满"}, ensure_ascii=False)
|
||||
body = json.dumps({"checks": _full_checks([True] * 11), "notes": "满"}, ensure_ascii=False)
|
||||
text = "这是我的评分:\n```json\n" + body + "\n```\n以上。"
|
||||
r = cheap_verify.parse_judge_output(text)
|
||||
assert r["degraded"] is False and r["score"] == 8
|
||||
assert r["degraded"] is False and r["score"] == 11
|
||||
|
||||
|
||||
def test_parse_garbage_degraded():
|
||||
"""完全非 JSON → degraded(score=None),不抛。"""
|
||||
"""完全非 JSON → degraded(score=None, groups=None),不抛。"""
|
||||
r = cheap_verify.parse_judge_output("完全不是 JSON 的一段话")
|
||||
assert r["degraded"] is True and r["score"] is None and r["max"] == 8
|
||||
assert r["degraded"] is True and r["score"] is None and r["max"] == 11 and r["groups"] is None
|
||||
|
||||
|
||||
def test_parse_none_and_empty_degraded():
|
||||
@ -81,6 +115,7 @@ def test_parse_coerce_and_positional():
|
||||
r = cheap_verify.parse_judge_output(json.dumps({"checks": checks}))
|
||||
assert r["degraded"] is False
|
||||
assert r["score"] == 2 # 前两条 "true"/1 命中,第三条「否」不命中,其余 False
|
||||
assert r["max"] == 11 and r["hits"][0]["layer"] == "L2" # 位置兜底也带 checklist 权威 layer
|
||||
|
||||
|
||||
# ───────────────────────── ② verify_richness 非阻塞契约(fake model,零真网络)─────────────────────────
|
||||
@ -107,7 +142,7 @@ class _FakeModel:
|
||||
return (11, 22)
|
||||
|
||||
|
||||
_SRC = "// game-logic.js\nfunction createGame(){ /* 进货 库存 解锁 playSfx 飘字 */ }"
|
||||
_SRC = "// game-logic.js\nfunction createGame(){ /* 进货 库存 解锁 playSfx 飘字 结算 remix */ }"
|
||||
|
||||
|
||||
def _skip_no_as(name):
|
||||
@ -115,12 +150,14 @@ def _skip_no_as(name):
|
||||
|
||||
|
||||
def test_verify_happy_path():
|
||||
"""fake model 返回合法 JSON → 结构化评分 + judgeTokens;非 degraded。"""
|
||||
"""fake model 返回合法 JSON → 结构化评分 + 三分组小计 + judgeTokens;非 degraded。"""
|
||||
if not _HAS_AS:
|
||||
return _skip_no_as("test_verify_happy_path")
|
||||
body = json.dumps({"checks": _full_checks([True] * 5 + [False] * 3), "notes": "不错"}, ensure_ascii=False)
|
||||
body = json.dumps({"checks": _full_checks([True] * 5 + [False] * 6), "notes": "不错"}, ensure_ascii=False)
|
||||
r = asyncio.run(cheap_verify.verify_richness("x", sources=_SRC, model=_FakeModel(text=body)))
|
||||
assert r["degraded"] is False and r["score"] == 5
|
||||
# 前 5 条命中 [T,T,T,T,T] 均属 L2 索引{0,1,3,4}? idx0,1L2 idx2L3 idx3,4L2 → L2=4 L3=1
|
||||
assert r["groups"]["L2"]["score"] == 4 and r["groups"]["L3"]["score"] == 1 and r["groups"]["L4"]["score"] == 0
|
||||
assert r["judgeTokens"]["total"] == 33
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user