From cbe42933d002fb59c666be80af33bc3c16ff143e Mon Sep 17 00:00:00 2001 From: lili Date: Mon, 22 Jun 2026 23:38:59 -0700 Subject: [PATCH] =?UTF-8?q?fix(saa):=20B=20=E6=94=B6=E5=B0=BE=C2=B7?= =?UTF-8?q?=E5=AE=A2=E8=A7=82=E9=97=A8=E8=B7=A8=E5=B1=82=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=20advisory=E2=80=94=E2=80=94objectiveGatesPass=20=E8=AF=BB=20v?= =?UTF-8?q?erdict.advisoryGates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:8ac4ccc8 的「未驱动 E_live/H 降 advisory」只落在 play.cdp.cjs(verdict.pass/ advisoryGates),未传到生成环消费层 SaaGenNodes.objectiveGatesPass——后者把 E_live 当 硬客观门、完全不读 advisoryGates。未驱动游戏(打地鼠 menu 态无 driver→phase 恒 menu→ _forensicsView distinctStates:1)E_live 每轮必挂→1+maxRepairs(5)=6 次 M3 假失败救场, 与既有 line651 注释里早修过的 H_progress「空跑 8 轮救场/302K token」同一类病(driver 依赖门绑架生成)。 修: - objectiveGatesPass / summarizeFailedGuards 读 verdict.advisoryGates 同口径降级。 advisory 门要求【存在】(保住 C2 崩溃安全:缺门=harness 未跑完=失败)但【允许 pass=false】 (未驱动无法公平验活性);driven 时 play.cdp.cjs 不列入 advisory → 自动恢复致命,回归不破。 - objectiveGatesPass 提包级可见(与 summarizeFailedGuards 同范式),供纯逻辑单测。 验证:SaaFullGraphE2eGateLogicTest 23/23 PASS(+5 advisory 案:未驱动 E_live→过/ driven E_live→挂/缺门→挂/全过/反馈跳 advisory)。e2e 已实证 verdict.pass=true、 E_live/H 列 advisory(advisory 跨层生效)。 注:e2e 绿基线仍被一个【独立·先存】问题阻断——过门游戏不产终态回调(player 节点 M3 首次在 pass 路被触达,疑 M3-vision 挂),非本修引入,下一步处置。 Co-Authored-By: Claude Opus 4.8 --- .../game/module/aigc/saa/SaaGenNodes.java | 41 +++++++++-- .../saa/SaaFullGraphE2eGateLogicTest.java | 69 +++++++++++++++++++ 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java index a7968f46..087925f5 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/saa/SaaGenNodes.java @@ -715,25 +715,51 @@ final class SaaGenNodes { *

遍历 verdict.guards 中出现的门,只对客观门要求 pass;driver 门跳过。无 guards 明细或未见任何客观门→ * 保守退回聚合 verdict.pass(避免误放空壳)。不改九门判定本身,只改生成环对其的消费口径。 */ - private static boolean objectiveGatesPass(JsonNode verdict) { + static boolean objectiveGatesPass(JsonNode verdict) { // 包级可见:供 SaaFullGraphE2eGateLogicTest 纯逻辑回归(与 summarizeFailedGuards 同范式)。 JsonNode guards = verdict.path("guards"); if (!guards.isObject() || guards.size() == 0) { return verdict.path("pass").asBoolean(false); } - // C2 修(CodeRabbit/Codex 双报,2026-06-20):客观门按【必达集】校验——5 门必须【全部存在且 pass】,缺任一即判失败。 + // B 收尾·跨层对齐 advisory(2026-06-22):play.cdp.cjs 在【未驱动】(spec 无 driver/inputs)时把 E_live/H_progress 降为 + // advisory(verdict.advisoryGates) 并据此算 verdict.pass。该降级必须同口径传到生成环消费层,否则未驱动游戏每轮卡 + // E_live 触发无谓救场重生成——实证打地鼠 menu 态无 driver→phase 恒 menu→_forensicsView distinctStates:1→E_live 必挂, + // 1+maxRepairs(5)=6 次 M3 生成全假失败(与 line 651 H_progress 空跑救场同一类病:driver 依赖门绑架生成)。 + java.util.Set advisory = readAdvisoryGates(verdict); + // C2 修(CodeRabbit/Codex 双报,2026-06-20):客观门按【必达集】校验——5 门必须【全部存在】,缺任一即判失败。 // 旧"遍历出现的门、出现才查"会被 harness 中途崩钻空子误判 PASS:play.cdp.cjs 的 runDriver(含 seek-food)在 // D_render/E_live 之前执行且未 try/catch,conc=12 下 CDP 抖动抛错→主 catch 只补 A_boot→verdict 只剩 // A_boot/C_frame/I_control→旧逻辑 sawObjective=true 放行=【崩掉、根本没测到渲染/活性的游戏被判过】。 - // 缺门=harness 未跑完=失败,才安全(旧 rc==0 口径反不会误判;本修补回该安全性、又不重新绑 driver 门)。 + // 缺门=harness 未跑完=失败,才安全。advisory 门要求【存在】(保住该崩溃安全)但【允许 pass=false】(未驱动无法公平验活性)。 for (String gate : OBJECTIVE_GATES) { JsonNode g = guards.path(gate); - if (g.isMissingNode() || !g.path("pass").asBoolean(false)) { - return false; // 客观门缺失或未过 → 生成硬门判失败。 + if (g.isMissingNode()) { + return false; // 客观门缺失=harness 未跑完 → 生成硬门判失败(C2 崩溃安全)。 + } + if (advisory.contains(gate)) { + continue; // advisory 门:存在即可,允许未过(如未驱动时 E_live;driven 恢复致命由 play.cdp.cjs 不列入 advisory 实现)。 + } + if (!g.path("pass").asBoolean(false)) { + return false; // 非 advisory 客观门未过 → 生成硬门判失败。 } } return true; } + /** 读 verdict.advisoryGates(play.cdp.cjs 在未驱动时降级的非致命门,如 E_live/H_progress)。缺/非数组→空集。 */ + private static java.util.Set readAdvisoryGates(JsonNode verdict) { + java.util.Set advisory = new java.util.HashSet<>(); + if (verdict == null) { + return advisory; + } + JsonNode adv = verdict.path("advisoryGates"); + if (adv.isArray()) { + for (JsonNode a : adv) { + advisory.add(a.asText()); + } + } + return advisory; + } + /** * 把失败的守卫摘成回喂文字(对齐 run.py:_verdict_feedback)。 */ @@ -742,6 +768,7 @@ final class SaaGenNodes { return "真玩阶段无 verdict 产出(疑似装载即崩/超时)。"; } List lines = new ArrayList<>(); + java.util.Set advisory = readAdvisoryGates(verdict); JsonNode guards = verdict.path("guards"); if (guards.isObject()) { Iterator> it = guards.fields(); @@ -751,6 +778,10 @@ final class SaaGenNodes { if (!GATE_ALL9 && !OBJECTIVE_GATES.contains(e.getKey())) { continue; } + // B 收尾:advisory 门(未驱动 E_live/H)非致命,不回喂救场——否则空喂"E_live 未过"驱动无谓重生成。 + if (!GATE_ALL9 && advisory.contains(e.getKey())) { + continue; + } if (!e.getValue().path("pass").asBoolean(false)) { String g = e.getValue().toString(); lines.add("- 守卫 " + e.getKey() + " 未过:" + (g.length() > 200 ? g.substring(0, 200) : g)); diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaFullGraphE2eGateLogicTest.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaFullGraphE2eGateLogicTest.java index 2f399bbe..8ea63cf6 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaFullGraphE2eGateLogicTest.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/test/java/com/wanxiang/huijing/game/module/aigc/saa/SaaFullGraphE2eGateLogicTest.java @@ -1,5 +1,7 @@ package com.wanxiang.huijing.game.module.aigc.saa; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.wanxiang.huijing.game.module.aigc.service.executor.AigcExecutorProperties; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -246,4 +248,71 @@ class SaaFullGraphE2eGateLogicTest { assertEquals(1, clamp.size(), "k<1 → 至少 1 槽(防御,不返空)"); assertEquals(4320, clamp.get(0)[0]); } + + // ═══════════════════════ B 收尾·客观门跨层对齐 advisory(2026-06-22 修:未驱动 E_live 误触救场)═══════════════════════ + // + // 背景:play.cdp.cjs 在【未驱动】(play-spec 无 driver/inputs)时把 E_live/H_progress 降为 advisory 并据此算 verdict.pass; + // 但生成环消费层 objectiveGatesPass 旧实现把 E_live 当硬客观门 → 未驱动游戏(打地鼠 menu 态)phase 恒 menu → + // distinctStates:1 → E_live 必挂 → 1+maxRepairs(5)=6 次 M3 假失败。本组锁住「客观门读 advisoryGates 同口径降级」。 + + /** 用 JSON 串构 verdict 节点(测试本地工具,避免手搭 ObjectNode)。 */ + private static JsonNode verdict(String json) { + try { + return new ObjectMapper().readTree(json); + } catch (Exception e) { + throw new RuntimeException("测试 verdict JSON 非法:" + e.getMessage(), e); + } + } + + @Test + @DisplayName("objectiveGatesPass:未驱动 advisory E_live 失败 → 仍 PASS(B 收尾跨层对齐,不再误触救场)") + void objectiveGatesPass_advisoryEliveFails_stillPass() { + // 打地鼠 menu 态无 driver:A-D 全过 + E_live 失败但列入 advisoryGates → 客观门应判过(与 verdict.pass 同口径)。 + JsonNode v = verdict("{\"pass\":true,\"advisoryGates\":[\"E_live\",\"H_progress\"]," + + "\"guards\":{\"A_boot\":{\"pass\":true},\"B_uncaught\":{\"pass\":true}," + + "\"C_frame\":{\"pass\":true},\"D_render\":{\"pass\":true}," + + "\"E_live\":{\"pass\":false,\"distinctStates\":1}}}"); + assertTrue(SaaGenNodes.objectiveGatesPass(v), "advisory E_live 失败不应否决客观门(否则未驱动游戏空跑救场)"); + } + + @Test + @DisplayName("objectiveGatesPass:driven(advisoryGates 空)E_live 失败 → FAIL(driven 恢复致命,回归不破)") + void objectiveGatesPass_drivenEliveFails_fail() { + // 有 driver 时 play.cdp.cjs 不把 E_live 列入 advisory → 客观门仍须 E_live 过,挂则判失败。 + JsonNode v = verdict("{\"pass\":false,\"advisoryGates\":[]," + + "\"guards\":{\"A_boot\":{\"pass\":true},\"B_uncaught\":{\"pass\":true}," + + "\"C_frame\":{\"pass\":true},\"D_render\":{\"pass\":true}," + + "\"E_live\":{\"pass\":false}}}"); + assertFalse(SaaGenNodes.objectiveGatesPass(v), "driven 时 E_live 仍是硬客观门"); + } + + @Test + @DisplayName("objectiveGatesPass:客观门缺失(harness 崩)→ FAIL(C2 崩溃安全不被 advisory 削弱)") + void objectiveGatesPass_missingGate_failEvenIfAdvisory() { + // D_render 缺失(harness 中途崩,没跑到渲染门):即便 advisory 含 D_render 也不得放行——缺=没跑完=失败。 + JsonNode v = verdict("{\"advisoryGates\":[\"D_render\",\"E_live\"]," + + "\"guards\":{\"A_boot\":{\"pass\":true},\"B_uncaught\":{\"pass\":true}," + + "\"C_frame\":{\"pass\":true},\"E_live\":{\"pass\":true}}}"); + assertFalse(SaaGenNodes.objectiveGatesPass(v), "客观门缺失=harness 未跑完,advisory 也不得放行(C2 崩溃安全)"); + } + + @Test + @DisplayName("objectiveGatesPass:全客观门过 + 无 advisory → PASS(happy)") + void objectiveGatesPass_allPass() { + JsonNode v = verdict("{\"guards\":{\"A_boot\":{\"pass\":true},\"B_uncaught\":{\"pass\":true}," + + "\"C_frame\":{\"pass\":true},\"D_render\":{\"pass\":true},\"E_live\":{\"pass\":true}}}"); + assertTrue(SaaGenNodes.objectiveGatesPass(v)); + } + + @Test + @DisplayName("summarizeFailedGuards:advisory 门不回喂救场;真硬门失败仍回喂") + void summarizeFailedGuards_skipsAdvisory() { + // advisory E_live 失败不进救场反馈(否则空喂"E_live 未过"驱动无谓重生成);真硬门 D_render 失败仍须回喂。 + JsonNode v = verdict("{\"advisoryGates\":[\"E_live\"]," + + "\"guards\":{\"A_boot\":{\"pass\":true},\"D_render\":{\"pass\":false}," + + "\"E_live\":{\"pass\":false,\"distinctStates\":1}}}"); + String fb = SaaGenNodes.summarizeFailedGuards(v); + assertTrue(fb.contains("D_render"), "真硬门 D_render 失败应回喂救场"); + assertFalse(fb.contains("E_live"), "advisory E_live 不应回喂救场"); + } }