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 不应回喂救场");
+ }
}