From 69307df8a50eb0caabbc7a4c0a6c757990e4ccf5 Mon Sep 17 00:00:00 2001 From: lili Date: Wed, 8 Jul 2026 23:27:22 -0700 Subject: [PATCH] =?UTF-8?q?fix(cheap-gen):=20=E4=BF=AE=20check=20=E5=81=87?= =?UTF-8?q?=E9=98=B4=E6=80=A7=E2=80=94=E2=80=94stripCode=20=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=E5=86=85=20//=20=E8=87=B4=E7=BA=A7=E8=81=94?= =?UTF-8?q?=E8=AF=AF=E6=8A=B9=E7=9C=9F=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit n=5 跨主题验证 trpg-cyber 失败根因坐实:赛博主题文案用 '> JACK IN // FLOOR 01' 这类 字符串内含 //。旧 stripCode 用 5 条独立正则(先剥行注释、再剥字符串),字符串里的 // 被 行注释正则误当注释、吞掉闭合引号 → 引号失衡 → 后续字符串正则跨行匹配 → 级联误抹 169 行真 代码(bundle.tick/_forensicsView/handleTap 一起抹)→ check 误报三红线缺失 → 模型明明写对 却被反复拒 → thrash 到步数硬顶熔断、烧 ¥13.84 失败。 根治:stripCode 改单遍左→右状态机(码/行注释/块注释/单引/双引/模板 六态),同一时刻只一种态、 嵌套的 // 与引号各归其态绝不越界——「注释与字符串互嵌」词法问题唯一正确解。逐字节保长、换行 保留(红线行号定位不变)。影响面广:任何 game-logic 字符串含 //(URL/路径/N//M/科技赛博文案) 都触发假阴性,部分历史失败率实为此 bug 冒充模型质量差。 验证:trpg-cyber check.ok false→true;heritage/narrative 无回归;amodel-gen 35/35 绿 (check-banned 加 2 条 //-in-string 回归守卫)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../amodel-gen/tests/check-banned.test.mjs | 32 +++++++++++++ game-runtime/tools/amodel-gen/tools.mjs | 47 +++++++++++++++---- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/game-runtime/tools/amodel-gen/tests/check-banned.test.mjs b/game-runtime/tools/amodel-gen/tests/check-banned.test.mjs index 230c1ea7..a1380fa7 100644 --- a/game-runtime/tools/amodel-gen/tests/check-banned.test.mjs +++ b/game-runtime/tools/amodel-gen/tests/check-banned.test.mjs @@ -102,3 +102,35 @@ test('④ 合规 ctx 写法零报', () => { 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)}`); +}); diff --git a/game-runtime/tools/amodel-gen/tools.mjs b/game-runtime/tools/amodel-gen/tools.mjs index c26f0544..e60f88ab 100644 --- a/game-runtime/tools/amodel-gen/tools.mjs +++ b/game-runtime/tools/amodel-gen/tools.mjs @@ -135,15 +135,46 @@ const BANNED = [ /** 去注释 + 字符串字面量,避免红线/结构正则误伤文档注释里的示例(如注释里写「禁用 Math.random()」)。 * 抹除时把内容换成【等量空格、保留换行】——结果串与原文行号一一对应(红线报错据此定位到行), - * 且长度不变、不误匹配(空格非 \w、非 `.`,既有的 matchAll/结构正则语义不变)。 */ + * 且长度不变、不误匹配(空格非 \w、非 `.`,既有的 matchAll/结构正则语义不变)。 + * + * 【为什么必须单遍扫描,不能用多条独立正则】(2026-07-08 n=5 收敛环 trpg-cyber 坐实的 check 假阴性根治): + * 旧版 5 条独立 .replace 先剥行注释、再剥字符串——但注释与字符串会互相嵌套:字符串里的 `//` + * (赛博主题 '> JACK IN // FLOOR 01' 这类)会被行注释正则误当注释、吞到行尾(含闭合引号)→ 引号失衡 + * → 后续字符串正则从游离引号起跨行匹配 → 级联误抹真代码(实测抹掉 169 行,连 bundle.tick/_forensicsView/ + * handleTap 一起抹)→ check 误报红线方法"缺失"→ 模型明明写对了却被反复拒 → thrash 到步数顶熔断、烧钱失败。 + * 单遍状态机逐字符扫,同一时刻只处于「码/行注释/块注释/单引/双引/模板」一种态,嵌套的 `//`、引号各归其态、 + * 绝不越界——这是「注释与字符串互嵌」这类词法问题唯一正确的解法。逐字节保长、换行保留(行号对齐不变)。 + */ function stripCode(s) { - const blank = (m) => m.replace(/[^\n]/g, ' '); // 非换行→空格、换行留 - return s - .replace(/\/\*[\s\S]*?\*\//g, blank) // 块注释(可跨行) - .replace(/\/\/[^\n]*/g, blank) // 行注释 - .replace(/'(?:[^'\\]|\\.)*'/g, blank) // 单引号串 - .replace(/"(?:[^"\\]|\\.)*"/g, blank) // 双引号串 - .replace(/`(?:[^`\\]|\\.)*`/g, blank); // 模板串(可跨行) + const out = []; + const n = s.length; + let state = 'code'; // code | line | block | sq(单引) | dq(双引) | tpl(模板) + for (let i = 0; i < n; i++) { + const c = s[i]; + const c2 = i + 1 < n ? s[i + 1] : ''; + if (state === 'code') { + if (c === '/' && c2 === '/') { state = 'line'; out.push(' '); i += 1; continue; } + if (c === '/' && c2 === '*') { state = 'block'; out.push(' '); i += 1; continue; } + if (c === "'") { state = 'sq'; out.push(' '); continue; } + if (c === '"') { state = 'dq'; out.push(' '); continue; } + if (c === '`') { state = 'tpl'; out.push(' '); continue; } + out.push(c); continue; // 正常码逐字节保留(正则字面量按码处理,与旧版同,不新增误伤面) + } + if (state === 'line') { // 行注释:遇换行收尾 + if (c === '\n') { state = 'code'; out.push('\n'); continue; } + out.push(' '); continue; + } + if (state === 'block') { // 块注释:遇 */ 收尾,换行保留 + if (c === '*' && c2 === '/') { state = 'code'; out.push(' '); i += 1; continue; } + out.push(c === '\n' ? '\n' : ' '); continue; + } + // 字符串三态(sq/dq/tpl):转义跳一对、遇匹配闭合退回码、换行保留(模板可跨行;单双引跨行容错) + const quote = state === 'sq' ? "'" : state === 'dq' ? '"' : '`'; + if (c === '\\') { out.push(' '); out.push(c2 === '\n' ? '\n' : (c2 ? ' ' : '')); i += 1; continue; } + if (c === quote) { state = 'code'; out.push(' '); continue; } + out.push(c === '\n' ? '\n' : ' '); continue; + } + return out.join(''); } /** 插件键 → api.d.ts 目录(host-config 注入的 canonical 键名;特殊:juice/save/palettePost/physics 名≠dir)。 */