- tsc 顾问门:typescript devDependency + tsc-advisory 测试档;顾问性(永不阻断), 金标 10 款零误伤抽验 - recipes/ 五篇高频场景索引(命中矩形/计时器/资产回退/场景机/结算演出,「抄这段形态 +头号病根」)+ littlejs-game-dev §0.5 卡壳就查接线 - 暗资产接线:plugin-capability-map(11 注入+1 取证+6 储备逐件裁定)+ engine-capabilities-brief(≤3KB 蒸馏)+ api.d.ts/game-host.d.ts 补插件指针; 三方一致性对账门 plugin-surface-gate.py(含 cwd 锚定修)挂 pre-commit+Gitea Actions - hard_genre_batch 参数化(五品类 n=5 批跑驱动,checkpoint 续跑) - n=5 基线台账入库(gitignore 例外 add -f,只入台账不入游戏产物): wax-baseline.jsonl 25 局逐局记录(含判定仪器订正批注:cap1500 重判/图像盲 M3 重判, 只加字段零改值)——终数:操作口径 14/25=56%、质量口径 18/25=72%; 品类 narrative/heritage 5/5、trpg 4/5、puzzle/sim-business 2/5(缺口逐局有名有姓, 根因见 07-10 v2 plan §1);旧「80%/39%」口径正式作废存照 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
200 lines
12 KiB
Python
200 lines
12 KiB
Python
"""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 与生产同读 genconfig(max_tokens 单一源)
|
||
|
||
# max_tokens 与生产 create 路(cheap_service_driver)同源 genconfig.model.max_tokens(2026-07-09 调 32K:16K 是自设限、
|
||
# 实测 M3 单轮能输出 17000+ tokens,inline 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(判定用独立档,与生成台账隔离,累计兜底要两者都算)。"""
|
||
gen = row.get("costRmb") or 0.0
|
||
j = row.get("judge") or {}
|
||
jc = j.get("costRmb") or 0.0
|
||
try:
|
||
return float(gen) + float(jc)
|
||
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, "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 {}
|
||
rich = summary.get("richness") or {}
|
||
layer = summary.get("failureLayer") or {} # classify_cheap_failure_layer 返回 {layer,reason,failedGates}
|
||
j = summary.get("judge") or {} # apply_gameplay_judge 写入(仅 run_gates∧finished 时有)
|
||
return {
|
||
"genre": genre, "gid": gid, "round": round_no,
|
||
"template": scaffold_template or "_template(通用)",
|
||
# ── 三个验收信号分开记(2026-07-09 新语义,别混):
|
||
"accepted": bool(summary.get("accepted")), # ★最终权威 = 预筛 ∧ 独立模型玩法判定
|
||
"verdictPass": v.get("pass"), # 预筛(机械九门),只是地板不是验收
|
||
"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,
|
||
# ── 失败归因三层(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 口径):
|
||
✅ accepted=True(最终权威过)
|
||
⚠️判定拒 预筛过(verdictPass=True)但判定拒(accepted=False)——机械门放行、独立判定逮住的坏游戏
|
||
finished-only 模型自称收敛(finished=True)但预筛未过(旧 FALSE-PASS 改名,新语义)
|
||
❌ 未收敛/harness 挂
|
||
"""
|
||
if r.get("accepted"):
|
||
return "✅"
|
||
if r.get("verdictPass") 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 {}
|
||
print(f"{_mark(r):>13s} r{round_no} {genre:14s} tpl={r.get('template','?')} "
|
||
f"accepted={r.get('accepted')} verdictPass={r.get('verdictPass')} finished={r.get('finished')} "
|
||
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')} 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 口径为主,verdictPass/finished 并列对照)──
|
||
n = len(session_rows)
|
||
acc = sum(1 for r in session_rows if r.get("accepted"))
|
||
vp = sum(1 for r in session_rows if r.get("verdictPass") is True)
|
||
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"))
|
||
print("\n========== 本次汇总(新验收语义)==========")
|
||
print(f">>> 真实率(accepted 最终权威):{acc}/{n} = {acc / n:.0%}" if n else ">>> 无完成局")
|
||
if n:
|
||
print(f">>> 对照:预筛过(verdictPass) {vp}/{n}={vp / n:.0%} | 收敛(finished) {fin}/{n}={fin / n:.0%} | 判定 degraded {deg}/{n}")
|
||
print(f">>> 差额 = 预筛过但判定拒 {vp - acc} 局(机械门放行、独立判定逮住);全 finished-only={fin - vp} 局(收敛但预筛未过)")
|
||
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())
|