fix(cheap-gen): 修 check 假阴性——stripCode 字符串内 // 致级联误抹真代码

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) <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-08 23:27:22 -07:00
parent c8a15db349
commit 69307df8a5
2 changed files with 71 additions and 8 deletions

View File

@ -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)}`);
});

View File

@ -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)。 */