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:
parent
c8a15db349
commit
69307df8a5
@ -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)}`);
|
||||
});
|
||||
|
||||
@ -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)。 */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user