W-G1 生成核心:brief→便宜模型写 GameHostFactory→esbuild __GameBundle iife→P1 bootGameHost
装载→本机 Chrome+CDP 七门真玩取证;失败回喂错因重试。只消费冻结契约,零改 P1/P2。
实测(deepseek-v4-flash 最便宜档):Pong/Simon/打地鼠 三款【七门全过真玩】,
单款 ¥0.01–0.03(远低于 ¥0.15 L1 闸门)。最优模型:flash 打底 + 难档升 deepseek-v4-pro(Simon 0 重试 ¥0.06)。
关键:实证「能生成代码 ≠ 能玩」——一处 prompt 输入坐标契约写错(误称 x/y∈[0,1],实为逻辑像素)
让生成游戏能编译/渲染/动但点击全失灵;Pong 因自走动画假绿混过、Simon 被正确判否。
为此加装 G 门(确定性 A/B 对照,隔离自走动画假绿)+ 修 prompt 后三款真过。
- harness:games/_wg1-gen/_shared/{entry-bundle 模板,index 模板,play.cdp.cjs 七门,serve-and-play.sh}(复用 ref buildRefHostConfig + browser-evidence.cjs)
- worker:wg1/gen-worker/worker/{_client(代理旁路),prompt(生成上下文包),validate(静态门+node --check),cost(¥折算),run(§3 编排+重试门)}
- golden-path 自检:ref 工厂 5/6(F=0 菜单态正确)+ probe 工厂 7/7 → harness 无假绿
- game-runtime/package.json:加 ws(CDP 驱动 devDep)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
4.4 KiB
Python
112 lines
4.4 KiB
Python
"""validate.py —— L1 校验闸门:从模型输出抽源码 + 静态契约检查 + node --check 语法门。
|
||
|
||
spike 无此模块(gate3 只算 token),故 L1 新建(worker-reusables 地图明示)。
|
||
两层:① 静态契约扫描(默认导出/五方法/禁用词)——快、拦低级违约;② node --check ESM 语法——拦编译失败。
|
||
"""
|
||
|
||
import re
|
||
import subprocess
|
||
import tempfile
|
||
import os
|
||
|
||
|
||
def extract_code(content):
|
||
"""从模型输出抽出 ```js 代码块;无 fence 则退而取最长 ``` 块;再无则用全文。"""
|
||
if not content:
|
||
return ""
|
||
# 优先 ```js / ```javascript
|
||
m = re.search(r"```(?:js|javascript)\s*\n(.*?)```", content, re.S | re.I)
|
||
if m:
|
||
return m.group(1).strip()
|
||
# 任意 ``` 块(取第一个)
|
||
m = re.search(r"```\s*\n(.*?)```", content, re.S)
|
||
if m:
|
||
return m.group(1).strip()
|
||
# 无 fence:若看着像代码(含 export default)直接用全文
|
||
if "export default" in content:
|
||
return content.strip()
|
||
return ""
|
||
|
||
|
||
def _strip_comments(src):
|
||
"""去行注释/块注释,避免禁用词扫描被注释里的字样误伤。"""
|
||
src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
|
||
src = re.sub(r"//[^\n]*", "", src)
|
||
return src
|
||
|
||
|
||
# 禁用模式(正则, 说明)——违反即【契约违约】,拦在打包前。
|
||
_FORBIDDEN = [
|
||
(r"(^|\n)\s*import\s", "禁 import:工厂必须自包含单文件(含禁 import littlejsengine)"),
|
||
(r"\bimport\s*\(", "禁动态 import()"),
|
||
(r"\brequire\s*\(", "禁 require"),
|
||
(r"littlejsengine", "禁引擎名字空间"),
|
||
(r"\bMath\.random\b", "禁 Math.random(用 ctx.random)"),
|
||
(r"\bDate\.now\b", "禁 Date.now(用 ctx.time.nowMs)"),
|
||
(r"\bperformance\.now\b", "禁 performance.now(用 ctx.time)"),
|
||
(r"\baddEventListener\b", "禁 addEventListener(用 ctx.getInput().on)"),
|
||
(r"\brequestAnimationFrame\b", "禁 requestAnimationFrame(宿主驱动)"),
|
||
(r"new\s+AudioContext", "禁 new AudioContext(用 ctx.getAudioContext)"),
|
||
(r"\bdocument\s*\.", "禁直接操作 document"),
|
||
(r"\bwindow\s*\.", "禁直接操作 window"),
|
||
]
|
||
|
||
|
||
def static_check(src):
|
||
"""静态契约检查。返回 (ok, errors[])。"""
|
||
errors = []
|
||
if not src or len(src.strip()) < 60:
|
||
return False, ["源码为空或过短(疑似未产出有效代码)"]
|
||
if "export default" not in src:
|
||
errors.append("缺少 export default(必须默认导出工厂函数)")
|
||
scan = _strip_comments(src)
|
||
# 五方法存在性(宽松:方法名作为标识符出现)。
|
||
for meth in ("init", "update", "render", "destroy"):
|
||
if not re.search(r"\b" + meth + r"\b", scan):
|
||
errors.append(f"缺少 {meth} 方法")
|
||
# 必须经 boot.ctx 取能力(至少出现 ctx. 用法)。
|
||
if "ctx" not in scan:
|
||
errors.append("未见 ctx 用法(能力须经 boot.ctx)")
|
||
# 必须至少用一次 getEngine(硬性验收点:真接线门要有信号)。
|
||
if "getEngine" not in scan:
|
||
errors.append("未调用 getEngine()(必须在某事件上用一次引擎能力,否则真接线门无信号)")
|
||
# 禁用词扫描。
|
||
for pat, desc in _FORBIDDEN:
|
||
if re.search(pat, scan):
|
||
errors.append("违禁:" + desc)
|
||
return (len(errors) == 0), errors
|
||
|
||
|
||
def node_syntax_check(src):
|
||
"""node --check ESM 语法门(拦编译失败)。返回 (ok, stderr)。"""
|
||
tmp = tempfile.NamedTemporaryFile("w", suffix=".mjs", delete=False, encoding="utf-8")
|
||
try:
|
||
tmp.write(src)
|
||
tmp.close()
|
||
r = subprocess.run(
|
||
["node", "--check", tmp.name],
|
||
capture_output=True, text=True, timeout=30,
|
||
)
|
||
if r.returncode == 0:
|
||
return True, ""
|
||
# 清理临时路径噪声,只留错因。
|
||
return False, (r.stderr or r.stdout or "node --check 失败").strip()
|
||
except Exception as e:
|
||
return False, f"node --check 异常:{e}"
|
||
finally:
|
||
try:
|
||
os.unlink(tmp.name)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def validate(src):
|
||
"""综合校验门。返回 (ok, errors[])。静态过了才跑 node --check(省时)。"""
|
||
ok, errors = static_check(src)
|
||
if not ok:
|
||
return False, errors
|
||
ok2, stderr = node_syntax_check(src)
|
||
if not ok2:
|
||
return False, ["语法错误(node --check):" + stderr[:500]]
|
||
return True, []
|