games-development-ai/cheap-worker/hard_genre_batch.py
lili 8d851ea716
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(cheap-gen): W-AXIS-V2 波2——取证契约退役+提示全加载链清洗,四门∧测试员为唯一验收链
opus 执行位交付、主会话四步验收过(三套测试亲跑/红线 diff 亲读/冒烟截图亲眼):

- 验收消费:check 新增源码级插件调用存在断言(_pluginCallPresenceErrors 词法层,
  接替 F_wiring 真接线护栏);_forensicsView「必须实现」拦截摘除,降级为可选调试
  接口(handleTap 输入契约与 ctx.log 日志纪律保留);gate_judge 放行措辞对齐四门
- play-spec/驱动器退役(逐件裁定 10 件):gen.mjs done 门自动产 play-spec 摘除、
  cheap_gates/cheap_studio/cheap_modify/service_driver 调用摘除、_read_driver_type
  改读 evidence/playtest/playtest.json;golden-spec 三工具(auto_vs_golden/bake_off/
  compare_node)显式封存;ensure_play_spec 两实现保留一个版本窗口(只摘调用不删码)
- prompt 全加载链清洗:cheap-system.md v1.6.3→1.7.0(删 _forensicsView 自动验收
  契约红线与全部 targets/occupied 语义,五法句/品类指路同步),cheap_roles 内置
  回退逐字节同步(test_roles 12/12 证 parity),modify 切片经运行时切块自动同步;
  5 品类 skills+littlejs 手册+recipes×2+模板 8 件+README 6 件教学清洗,真设计
  价值(单击 casual/可解性质/双层反馈/latch)保留
- 批账:hard_genre_batch/xtheme 行加 playtest 段,_row_total_cost 修 shadow 下
  测试员成本漏算(v2 取 max 防双计)

验收(主会话亲验):pytest cheap-worker 512 绿 + tier2 141 绿 + node 49/49;
25 局基线新旧口径重放对照——真坏 5 局全拦(floor 拦 2/测试员拦 3)、无一例旧拦
新放,翻案 2 局均为破案坐实的仪器误杀;n=3 冒烟(mode=v2)全链绿:w2v2-sim
harness verdict.pass=True 而测试员 reject(机械放行测试员逮住,盘上双证)、
w2v2-sim-fix 现象反馈两轮回喂→fix2 收敛(配方显示修成,局内截图亲眼);
rg 零残留(模板残留均为「验收不读它」的降级调试接口合规形态);真跑 ¥8.4。

已知余项:repairFeedback 有产地、Service 续修环消费者未接(测试员 reject 不入
RepairMiddleware,续修判据仍四门)——是否接入随波3 校准数据一并裁;v1 配置级
回退窗口关闭为创始人知悉项,genconfig 默认 mode 仍 shadow、切 v2 归创始人。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:21:04 -07:00

237 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""hard_genre_batch.py — 便宜档【5 已覆盖品类】n=5 重测基线批跑W-AXIS 收尾)。
2026-07-09 换轴后重锚:验收权威 = accepted = 机械九门预筛 ∧ 独立模型玩法判定(glm-5.2 看真玩证据),
verdictPass 只是预筛地板、finished 只是模型自称收敛。本脚本作废换轴前那批全绿的污染数字(旧口径把
「九门 pass」当验收通过),在新链路(turns.jsonl 真相层 + per-run 归档 + 三层归因)与新语义下重测。
品类 = 5 个【有 per-genre 模板】的已覆盖品类(heritage/trpg/sim-business/puzzle/narrative);idle/action
无模板品类不入本轮(归 W-GENRE)。每品类各跑 n=5 局(distinct gid `hard-<genre>-r<round>`,绝不同 gid 重跑
洗数字——同 gid 会触发归档覆盖)。串行跑(本机 CPU+chrome 负载,别开并发)。
跑法(分级防无人值守烧钱):
冒烟 n=1×5: cheap-worker/.venv/bin/python cheap-worker/hard_genre_batch.py 1 1
全量补 r2-5: cheap-worker/.venv/bin/python cheap-worker/hard_genre_batch.py 4 2
参数:argv[1]=本次跑几轮(默认 1) argv[2]=起始轮号(默认 1);gid 后缀 = r<轮号>。
env:NO_PROXY(网关直连绕代理) + NEWAPI_KEY(_bootstrap 自动从凭据档解析)。
每局逐行追加 results/wax-baseline.jsonl(断点续跑不丢已完成局);累计成本超 ¥200 硬停(PARTIAL)。
"""
import asyncio
import json
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import cheap_studio # noqa: E402
from cheap_genre_route import route_genre # noqa: E402 brief→per-genre 黄金模板路由(对齐生产 create 路)
from cheap_roles import SCAFFOLD_DESC_BY_TEMPLATE # noqa: E402 per-genre 起点一句话描述
from worker import genconfig # noqa: E402 与生产同读 genconfigmax_tokens 单一源)
# max_tokens 与生产 create 路cheap_service_driver同源 genconfig.model.max_tokens2026-07-09 调 32K16K 是自设限、
# 实测 M3 单轮能输出 17000+ tokensinline thinking 吃额度致大文件整写截断——给 thinking+代码留足)。别用 run_studio 的 16K 默认。
_MAXTOK = genconfig.get("model", "max_tokens", 16000)
# 累计成本硬停线(¥):无人值守跑 25 局,任何一轮累计越线立即停机 PARTIAL(单局另有 ¥10 软/¥15 硬 breaker,此为跨局兜底)。
_HARD_STOP_RMB = 200.0
# 结果逐行落盘(断点续跑不丢):每完成一局即 append 一行 JSON。
_JSONL = Path(__file__).resolve().parent / "results" / "wax-baseline.jsonl"
# 5 个【已覆盖品类】brief——各命中一个 per-genre 黄金模板(route_genre 关键词确定性路由),取自批跑器既有 brief 池:
# narrative/trpg/heritage 用同题 brief(HARD 池);puzzle/sim-business 用跨主题 brief(XTHEME 池,该两类同题无 brief)。
# 路由核对:文字冒险/分支→story、掷骰→trpg、非遗→feiyi、华容道/解谜→puzzle、经营/奶茶店→shop。
COVERED_BRIEFS = [
("narrative", "一个分支选择推进的互动文字冒险:每屏一段剧情+2-3个选项,选择影响走向,最终3个不同结局,有结局图鉴"),
("trpg", "一个掷骰战斗的地牢爬塔:每层遇一个敌人,点击掷骰(可见骰点)按攻防结算,击败进下一层,失败结算,每3层升级选天赋"),
("heritage", "一个还原传统榫卯木工的非遗小游戏:按工序节拍(选料→画线→凿卯→修整)逐步操作,每步有节奏判定,完成得成品评分"),
("puzzle", "一个数字华容道解谜:滑动数字块把它们按序归位,规则逐关递进,卡壳给提示,通关计步数炫耀成绩"),
("sim-business", "一个经营奶茶店:顾客排队点单,按配方调对饮品上对得分,收银攒钱升级解锁新品与自动出杯设备"),
]
def _row_total_cost(row: dict) -> float:
"""一局的总成本 = 生成 costRmb + 判定 judge.costRmb + 测试员 playtest.costRmb(W-AXIS-V2 波2)。
v2 模式下 judge 段镜像 playtest 裁决、两者 costRmb 同值——取 max 只计一次(防同一笔钱双计);
shadow 模式下 judge 是判定器成本、playtest 是测试员成本,两笔真实分立——相加。据 acceptanceVersion 分辨。
"""
gen = row.get("costRmb") or 0.0
j = row.get("judge") or {}
pt = row.get("playtest") or {}
jc = j.get("costRmb") or 0.0
pc = pt.get("costRmb") or 0.0
try:
if row.get("acceptanceVersion") == "v2":
extra = max(float(jc or 0.0), float(pc or 0.0)) # v2:judge 段镜像 playtest,同一笔钱别双计
else:
extra = float(jc or 0.0) + float(pc or 0.0) # shadow/v1:判定器与测试员各自真花,分立累计
return float(gen) + extra
except (TypeError, ValueError):
return 0.0
def _prior_cumulative() -> float:
"""读已落盘 JSONL 里所有历史局的累计成本(跨多次调用续跑时,¥200 兜底要含之前轮次已花)。"""
if not _JSONL.is_file():
return 0.0
total = 0.0
for line in _JSONL.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
total += _row_total_cost(json.loads(line))
except json.JSONDecodeError:
continue
return total
async def _run_one(genre, brief, port, cdp, round_no):
"""跑一局并返回批次账行(串行,无 semaphore)。gid=hard-<genre>-r<round>,distinct、绝不同 gid 重跑。"""
gid = f"hard-{genre}-r{round_no}"
t0 = time.time()
# 对齐生产 create 路:brief→route_genre 选 per-genre 黄金模板作起点 + scaffold_desc 描述引导。
scaffold_template, routed_genre = route_genre(brief)
# 不写锁(2026-07-08/09 创始人纠偏,W-AXIS 波2 拆 reskin 管路):game-logic/core/render/assets 全放开,
# 只锁绝对通用 plumbing=L1_FIXED;写坏靠 harness 门 + 独立判定兜、不靠限死模型的手。
write_whitelist = None
scaffold_desc = SCAFFOLD_DESC_BY_TEMPLATE.get(scaffold_template) if scaffold_template else None
try:
summary = await cheap_studio.run_studio(
gid, brief, run_gates=True, port=port, cdp_port=cdp,
scaffold_template=scaffold_template, scaffold_desc=scaffold_desc,
write_whitelist=write_whitelist, genre=routed_genre, max_tokens=_MAXTOK)
except Exception as e: # noqa: BLE001 harness 级异常也原样计入基线(失败也是基线的一部分)
return {"genre": genre, "gid": gid, "round": round_no,
"accepted": False, "floorPass": None, "acceptanceVersion": None,
"verdictPass": None, "finished": False, "ok": False,
"judge": None, "fail": f"{type(e).__name__}: {e}", "wallSec": round(time.time() - t0, 1)}
v = summary.get("verdict") or {}
floor = summary.get("floor") or {} # 四门投影(run_acceptance 落;缺则空 → floorPass=None)
rich = summary.get("richness") or {}
layer = summary.get("failureLayer") or {} # classify_cheap_failure_layer 返回 {layer,reason,failedGates}
j = summary.get("judge") or {} # run_acceptance 写入(仅 run_gates∧finished 时有)
pt = summary.get("playtest") or {} # 测试员段(W-AXIS-V2 波2:v2/shadow 时有,批账观测口径)
return {
"genre": genre, "gid": gid, "round": round_no,
"template": scaffold_template or "_template(通用)",
# ── 验收信号分开记(W-AXIS-V2 波1 语义,别混):
"accepted": bool(summary.get("accepted")), # ★最终权威 = 四门投影 ∧ 测试员真玩(v2)/ 旧口径(shadow/v1)
"floorPass": floor.get("pass"), # ★预筛权威 = 四门投影(A/B/C/D),取代 verdictPass 作预筛口径
"acceptanceVersion": summary.get("acceptanceVersion"), # v2/shadow/v1(shadow 与 v2 行靠它区分,防波3对账串数)
"shadowV2Accepted": summary.get("shadowV2Accepted"), # shadow 模式下 v2 若生效的影子裁决(新旧口径对照)
"verdictPass": v.get("pass"), # harness 原生九门/七门 AND,窗口内仅对照打印、【不作判定输入】
"finished": bool(summary.get("finished")), # 模型自称收敛(check+build 绿),不是验收
"ok": bool(summary.get("ok")), # run_studio 收口 ok(blocking 下 = accepted)
# ── 判定段(verdict/rejectClasses/degraded/costRmb + 诊断):
"judge": {
"ran": j.get("ran"), "verdict": j.get("verdict"),
"rejectClasses": j.get("rejectClasses"), "degraded": j.get("degraded"),
"costRmb": j.get("costRmb"), "reason": j.get("reason"),
"model": j.get("model"), "frames": j.get("frames"),
} if j else None,
# ── 测试员段(W-AXIS-V2 波2 §4:裁决/degraded/成本/rolls/首局三字段——批账观测口径):
"playtest": {
"accepted": pt.get("accepted"), "verdict": pt.get("verdict"),
"degraded": pt.get("degraded"), "imageBlind": pt.get("imageBlind"),
"rollCount": pt.get("rollCount"), "costRmb": pt.get("costRmb"),
"reason": pt.get("reason"), "summary": pt.get("summary"),
"firstPlay": pt.get("firstPlay"), "model": pt.get("model"),
} if pt else None,
# ── 失败归因三层(driver_contract/mechanical/gameplay/none)+ 归档指针:
"failedGates": v.get("failedGates"),
"failureLayer": layer.get("layer"),
"failureReason": layer.get("reason"),
"archivedPriorRunTo": summary.get("archivedPriorRunTo"), # distinct gid 首跑恒 None
"attempts": summary.get("attempts"),
"breaker": summary.get("breaker"),
"costRmb": summary.get("costRmb"), # 生成成本(不含判定)
"richness": rich.get("score"), "richMax": rich.get("max"),
"wallSec": round(time.time() - t0, 1),
}
def _mark(r: dict) -> str:
"""一局的达标标记(accepted 口径,W-AXIS-V2 波1):
✅ accepted=True(最终权威过)
⚠️测试员拒 四门过(floorPass=True)但测试员拒(accepted=False)——机械门放行、测试员逮住的坏游戏
finished-only 模型自称收敛(finished=True)但四门未过
❌ 未收敛/harness 挂
"""
if r.get("accepted"):
return ""
if r.get("floorPass") is True:
return "⚠️测试员拒"
if r.get("finished"):
return "finished-only"
return ""
async def main():
n_rounds = int(sys.argv[1]) if len(sys.argv) > 1 else 1
start_round = int(sys.argv[2]) if len(sys.argv) > 2 else 1
_JSONL.parent.mkdir(exist_ok=True)
cumulative = _prior_cumulative()
print(f">>> W-AXIS n=5 重测基线:本次跑 {n_rounds} 轮(r{start_round}..r{start_round + n_rounds - 1})× {len(COVERED_BRIEFS)} 品类,"
f"串行;已落盘历史累计成本 ¥{cumulative:.2f}(硬停线 ¥{_HARD_STOP_RMB:.0f})")
session_rows = []
stopped = False
for round_no in range(start_round, start_round + n_rounds):
if stopped:
break
for cat_i, (genre, brief) in enumerate(COVERED_BRIEFS):
# 串行:每局固定 port(同 index),前一局 chrome 已收才起下一局。
r = await _run_one(genre, brief, 4340 + cat_i * 2, 9242 + cat_i * 2, round_no)
session_rows.append(r)
# 逐行落盘(断点续跑不丢)。
with _JSONL.open("a", encoding="utf-8") as f:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
cumulative += _row_total_cost(r)
j = r.get("judge") or {}
pt = r.get("playtest") or {}
print(f"{_mark(r):>13s} r{round_no} {genre:14s} tpl={r.get('template','?')} "
f"accepted={r.get('accepted')} floorPass={r.get('floorPass')} ver={r.get('acceptanceVersion')} "
f"verdictPass(对照)={r.get('verdictPass')} finished={r.get('finished')} "
f"playtest={pt.get('verdict')}/rolls={pt.get('rollCount')} ptDegraded={pt.get('degraded')} "
f"judge={j.get('verdict')}/{j.get('rejectClasses')} degraded={j.get('degraded')} "
f"layer={r.get('failureLayer')} attempts={r.get('attempts')} "
f"genCost=¥{r.get('costRmb')} judgeCost=¥{j.get('costRmb')} ptCost=¥{pt.get('costRmb')} wall={r.get('wallSec')}s "
f"累计=¥{cumulative:.2f} {('FAIL='+r['fail']) if r.get('fail') else ''}")
# ¥200 跨局硬停:立即停机,已完成局全在 JSONL,报 PARTIAL。
if cumulative > _HARD_STOP_RMB:
print(f"\n!!! 累计成本 ¥{cumulative:.2f} 越硬停线 ¥{_HARD_STOP_RMB:.0f} → 停机 PARTIAL(已完成 {len(session_rows)} 局本次)")
stopped = True
break
# ── 本次汇总(accepted 口径为主,floorPass/finished 并列;verdictPass 仅对照、不作判定输入)──
n = len(session_rows)
acc = sum(1 for r in session_rows if r.get("accepted"))
fp = sum(1 for r in session_rows if r.get("floorPass") is True) # 预筛权威 = 四门投影
vp = sum(1 for r in session_rows if r.get("verdictPass") is True) # harness 原生九门/七门,仅对照
fin = sum(1 for r in session_rows if r.get("finished"))
deg = sum(1 for r in session_rows if (r.get("judge") or {}).get("degraded")
or (r.get("playtest") or {}).get("degraded"))
print("\n========== 本次汇总(W-AXIS-V2 验收语义)==========")
print(f">>> 真实率(accepted 最终权威):{acc}/{n} = {acc / n:.0%}" if n else ">>> 无完成局")
if n:
print(f">>> 预筛(floorPass 四门投影):{fp}/{n}={fp / n:.0%} | 收敛(finished){fin}/{n}={fin / n:.0%} | degraded {deg}/{n}")
print(f">>> 对照(harness 原生 verdictPass,不作判定输入):{vp}/{n}={vp / n:.0%}")
print(f">>> 差额 = 四门过但测试员拒 {fp - acc} 局(机械门放行、测试员逮住)")
# shadow 灰度对照表(§4/波1 验收):shadow 行的旧口径 accepted vs v2 影子裁决(逐局对账,防波3串数)。
shadow_rows = [r for r in session_rows if r.get("acceptanceVersion") == "shadow"
and r.get("shadowV2Accepted") is not None]
if shadow_rows:
old_acc = sum(1 for r in shadow_rows if r.get("accepted"))
v2_acc = sum(1 for r in shadow_rows if r.get("shadowV2Accepted"))
print(f">>> shadow 对照表:{len(shadow_rows)} 局灰度,旧口径 accepted={old_acc} vs v2 影子 accepted={v2_acc}")
print(f">>> 累计成本(本次+历史)= ¥{cumulative:.2f}")
# 本次聚合另存一份(便于取用);逐局真相仍以 JSONL 为准。
out = _JSONL.parent / f"wax-baseline-r{start_round}-{start_round + n_rounds - 1}.json"
out.write_text(json.dumps(session_rows, ensure_ascii=False, indent=2), encoding="utf-8")
print(f">>> 本次结果 → {out};逐局全量 → {_JSONL}")
if __name__ == "__main__":
asyncio.run(main())