lili 882ca3f357 feat(cheap-gen): W-AXIS 波2 判定语义重置——独立模型玩法判定阻断+拆残笼+判定档切 M3
裁定三落地:accepted = 机械预筛 ∧ 独立模型玩法判定,九门 pass 降为「未见明显死」预筛。

- 波2a 玩法判定器(cheap_verify.py):读 brief+run-summary+全程截图(含新增局中连拍
  midplay-1..N,play.cdp.cjs 只读并发拍、不进任何门判据),布尔裁 broken/hollow/off_brief,
  fail-closed;判定真相层落 evidence/judge.json + verdict.json 写回 accepted/judge 段;
  金标夹具 fixtures/judge-golden/ + judge_golden.py 校准跑批
- 波2b 拆残笼:check 词法 tokenizer 替换 stripCode 正则(字符串内 // 假阴根修,292 在册
  源零差异);prompt v1.6.3 外科清洗(两层奖励→纯设计语/删盲驱动器围设计/删熔断恐吓),
  registry 热取==内置逐字节一致(cheap_roles 内置回退同步);edit_file 空白归一+近似片段提示
- 判定器补修:空输出有界重试一次(glm 思考吃光 max_tokens 时 content 空)+重试放大公式
  反缩 bug 修(旧 min(×2,6000) 大 base 反缩)+finishReason 落盘;imagesSeen 模型自报+
  盲检微扰重掷、两掷均盲=仪器故障 degraded(闲鱼中转静默丢图实锤,不再错杀 hollow)
- 判定档切 MiniMax-M3(创始人 2026-07-10 拍板,裁定三①边界随 v2 plan 修订):
  generation.yaml judge.model+max_tokens=202752(512K 网关实探 400 拒,取实探上限);
  3 例盲案 M3 重判全 accept、¥0.026/局
- 三路消费对齐:CLI 预筛语义修(cheap_studio 喂 verdict.pass 而非 finished,违裁定三的
  放行已纠)/Service 路 apply_gameplay_judge 显式 prefilter 参数/result_out 权威改读
  accepted(无键回落预筛,旧产物兼容)+trace.gameplayJudge additive 透传;
  tier2 gate_judge/genconfig 同步判定配置与消费

测试:test_gameplay_judge 33 项新增,全仓 pytest 480 绿;真判定闭环 accept ¥0.061。

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

182 lines
10 KiB
JavaScript
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.

/**
* check-banned.test.mjs — amodel-gen 红线判定 _bannedLineErrors() 单测(死圈修复 2026-07-08 fix②-check)
* owner:便宜档生成质量线 跑法:node --test tools/amodel-gen/tests/check-banned.test.mjs(cwd 任意)
*
* 【守的不变量】
* ① 裸时间/随机/DOM(Math.random/Date.now/performance.now/setTimeout/setInterval/rAF/AudioContext/addEventListener)命中即报;
* ② 报错带【确切行号】——跨行块注释在前也不漂(旧 stripCode 把块注释塌成单空格会致行号漂,本测正是回归它);
* ③ 函数内的回退裸调、typeof 守卫后的裸调、任意嵌套层同样命中(正则按纯文本扫);
* ④ 零误杀:注释 / 字符串里的 API 名(经 stripCode)不触发;合规 ctx 写法不报。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { _bannedLineErrors, _bootCtxLine, _getInputLine, lexTokens } from '../tools.mjs';
/** 是否报了某红线(按正则 source 关键片段匹配)。 */
const hasBanned = (errs, frag) => errs.some((e) => e.includes(frag));
// ───────── ① 基本命中 + 行号 ─────────
test('① Math.random 命中并报出确切行号', () => {
const src = `export function createGame(){\n function rng(){ return Math.random(); }\n return { _forensicsView(){}, handleTap(){} };\n}`;
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.equal(errs.length, 1, JSON.stringify(errs));
assert.ok(errs[0].includes('第 2 行'), `应报第 2 行:${errs[0]}`);
assert.ok(errs[0].includes('Math\\.random'), '应含被禁正则');
assert.ok(errs[0].includes('edit_file'), '应给 edit_file 补救指向');
});
test('① Date.now / setTimeout / addEventListener 各自命中', () => {
const cases = [
[`const t = Date.now();`, 'Date\\.now'],
[`setTimeout(() => {}, 100);`, 'setTimeout'],
[`window.addEventListener('click', h);`, 'addEventListener'],
[`const c = new AudioContext();`, 'AudioContext'],
];
for (const [line, frag] of cases) {
const errs = _bannedLineErrors(`export function createGame(){\n ${line}\n}`, 'render.js');
assert.ok(hasBanned(errs, frag), `应命中 ${frag}:${line}`);
}
});
// ───────── ② 行号不因跨行块注释而漂(回归旧 stripCode 塌行 bug) ─────────
test('② 跨行块注释在违规行之前 → 行号不漂', () => {
// 块注释占第 1-3 行(旧实现塌成单空格会让后续行号整体上移);Math.random 在第 5 行。
const src = [
'/* 多行', // 1
' 块', // 2
' 注释 */', // 3
'export function createGame(){', // 4
' const r = Math.random();', // 5
' return { _forensicsView(){}, handleTap(){} };', // 6
'}', // 7
].join('\n');
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.equal(errs.length, 1, JSON.stringify(errs));
assert.ok(errs[0].includes('第 5 行'), `应报第 5 行(不漂):${errs[0]}`);
});
test('② 多处命中 → 逐行列出', () => {
const src = [
'export function createGame(){', // 1
' const a = Math.random();', // 2
' const b = 1;', // 3
' const c = Math.random();', // 4
' return {};', // 5
'}',
].join('\n');
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.equal(errs.length, 1, '同一红线合并为一条');
assert.ok(errs[0].includes('第 2/4 行'), `应列 2/4:${errs[0]}`);
});
// ───────── ③ 函数内 / typeof 守卫后的回退裸调同样命中 ─────────
test('③ typeof 守卫后的回退裸调仍命中(别靠守卫绕)', () => {
const src = `export function createGame(){\n function rng(){ return (typeof ctx !== 'undefined' && ctx.random) ? ctx.random.next() : Math.random(); }\n}`;
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.ok(hasBanned(errs, 'Math\\.random'), '守卫后的回退裸调应命中');
assert.ok(errs[0].includes('第 2 行'), errs[0]);
});
// ───────── ④ 零误杀 ─────────
test('④ 注释 / 字符串里的 API 名不触发', () => {
const src = [
'// 别写 Math.random(),要用 ctx.random.next()', // 行注释
'/* 反例:const t = Date.now() 也禁 */', // 块注释
'export function createGame(){',
" const tip = '禁用 setTimeout(fn, 100)';", // 字符串
' const r = ctx.random.next();', // 合规
' return { _forensicsView(){}, handleTap(){} };',
'}',
].join('\n');
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.equal(errs.length, 0, `注释/字符串里的 API 名不应触发:${JSON.stringify(errs)}`);
});
test('④ 合规 ctx 写法零报', () => {
const src = `export function createGame(){\n const r = ctx.random.next();\n const t = ctx.time.nowMs();\n return { _forensicsView(){}, handleTap(){} };\n}`;
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.equal(errs.length, 0, JSON.stringify(errs));
});
// ───────── ⑤ 字符串内的 // 不 desync stripCode(2026-07-08 trpg-cyber 假阴性根治回归) ─────────
test('⑤ 字符串内含 // 不吞后续真代码(赛博主题回归)', () => {
// 赛博主题实测:'> JACK IN // FLOOR 01' 字符串里的 //,旧 stripCode 行注释先剥、吞掉闭合引号 →
// 引号失衡 → 后续字符串正则跨行匹配 → 级联误抹真代码(实测抹 169 行,连 bundle.tick/handleTap 一起抹)
// → check 误报红线缺失 → 模型明明写对却被反复拒 → thrash 到步数顶熔断失败。回归:字符串后的真
// Math.random 仍须命中且报对行号(证明字符串内 // 未 desync 后续码)。
const src = [
'export function createGame(){', // 1
" const banner = '> JACK IN // FLOOR 01';", // 2 // 在字符串内
' const r = Math.random();', // 3 真违规,须命中
' return { _forensicsView(){}, handleTap(){} };', // 4
'}', // 5
].join('\n');
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.ok(hasBanned(errs, 'Math\\.random'), `字符串内 // 不应吞掉后续真代码:${JSON.stringify(errs)}`);
assert.ok(errs.some((e) => e.includes('第 3 行')), `Math.random 应报第 3 行:${JSON.stringify(errs)}`);
});
test('⑤ 字符串内的 // 后接注释形态的文本零误杀', () => {
// 双引号串含 //,且串内出现禁用 API 名(Date.now);既不该吞后续码、串内 API 名也不该误报。
const src = [
'export function createGame(){', // 1
' const url = "sys://net/Date.now() 只是文案 // 提示";', // 2 串内 // + API 名
' const t = ctx.time.nowMs();', // 3 合规
' return { _forensicsView(){}, handleTap(){} };', // 4
'}', // 5
].join('\n');
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.equal(errs.length, 0, `串内 // 与 API 名都不该报,后续合规码也不该被误伤:${JSON.stringify(errs)}`);
});
// ───────── ⑥ 词法真实版新增能力(W-AXIS 波2:模板插值裸调 / boot.ctx / getInput 行号)─────────
test('⑥ 模板插值 ${} 内的裸调照样命中(词法真实,非纯文本近似)', () => {
// 旧 stripCode 把整段模板(含 ${})抹成空白 → 插值里的 Math.random 被漏报;词法版把 ${} 内按码扫描 → 命中。
const src = [
'export function createGame(){', // 1
' const label = `种子:${Math.random()}`;', // 2 插值内真实裸调
' return { _forensicsView(){}, handleTap(){} };', // 3
'}', // 4
].join('\n');
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.ok(errs.some((e) => e.includes('Math\\.random')), `模板插值内的裸调应命中:${JSON.stringify(errs)}`);
assert.ok(errs.some((e) => e.includes('第 2 行')), `应报第 2 行:${JSON.stringify(errs)}`);
});
test('⑥ 模板纯文本(非插值)里的 API 名不误命中', () => {
const src = 'export function createGame(){\n const tip = `别写 Math.random() 或 Date.now()`;\n const r = ctx.random.next();\n return { _forensicsView(){}, handleTap(){} };\n}';
const errs = _bannedLineErrors(src, 'game-logic.js');
assert.equal(errs.length, 0, `模板纯文本里的 API 名不该报:${JSON.stringify(errs)}`);
});
test('⑥ _bootCtxLine:.boot.ctx 双层误用命中并给行号,正确 boot.ctx 零命中', () => {
const bad = 'export function createGame(){\n init(boot){ const ctx = boot.boot.ctx; }\n}';
assert.equal(_bootCtxLine(bad), 2, '.boot.ctx 应命中第 2 行');
const good = 'export function createGame(){\n init(boot){ const ctx = boot.ctx; ctx.log("t","x"); }\n}';
assert.equal(_bootCtxLine(good), 0, '正确 boot.ctx 不该命中');
// 字符串里的 .boot.ctx 文案不误命中
const strCase = 'export function createGame(){\n const tip = "别写 x.boot.ctx";\n}';
assert.equal(_bootCtxLine(strCase), 0, '字符串里的 .boot.ctx 不该命中');
});
test('⑥ _getInputLine:getInput( 命中并给行号,字符串里的 getInput 不误命中', () => {
const bad = 'export function createGame(){\n update(dt){ const i = ctx.getInput(); }\n}';
assert.equal(_getInputLine(bad), 2, 'getInput( 应命中第 2 行');
const strCase = 'export function createGame(){\n const tip = "不要调 getInput()";\n}';
assert.equal(_getInputLine(strCase), 0, '字符串里的 getInput 不该命中');
});
test('⑥ lexTokens:字符串/注释不产 token,码区标识符产 token', () => {
const toks = lexTokens('const a = "Math.random"; // Date.now\n foo.bar');
const ids = toks.filter((t) => t.k === 'id').map((t) => t.v);
assert.ok(ids.includes('const') && ids.includes('a') && ids.includes('foo') && ids.includes('bar'), JSON.stringify(ids));
assert.ok(!ids.includes('Math') && !ids.includes('random') && !ids.includes('Date') && !ids.includes('now'), `字符串/注释内的名不该成 token:${JSON.stringify(ids)}`);
});