fix(saa): B 收尾·客观门跨层对齐 advisory——objectiveGatesPass 读 verdict.advisoryGates

根因: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 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-22 23:38:59 -07:00
parent 8ac4ccc861
commit cbe42933d0
2 changed files with 105 additions and 5 deletions

View File

@ -715,25 +715,51 @@ final class SaaGenNodes {
* <p>遍历 verdict.guards 中出现的门,只对客观门要求 pass;driver 门跳过。无 guards 明细或未见任何客观门→ * <p>遍历 verdict.guards 中出现的门,只对客观门要求 pass;driver 门跳过。无 guards 明细或未见任何客观门→
* 保守退回聚合 verdict.pass(避免误放空壳)。<b>不改九门判定本身</b>,只改生成环对其的消费口径。 * 保守退回聚合 verdict.pass(避免误放空壳)。<b>不改九门判定本身</b>,只改生成环对其的消费口径。
*/ */
private static boolean objectiveGatesPass(JsonNode verdict) { static boolean objectiveGatesPass(JsonNode verdict) { // 包级可见:供 SaaFullGraphE2eGateLogicTest 纯逻辑回归(与 summarizeFailedGuards 同范式)。
JsonNode guards = verdict.path("guards"); JsonNode guards = verdict.path("guards");
if (!guards.isObject() || guards.size() == 0) { if (!guards.isObject() || guards.size() == 0) {
return verdict.path("pass").asBoolean(false); 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<String> advisory = readAdvisoryGates(verdict);
// C2 修(CodeRabbit/Codex 双报,2026-06-20):客观门按【必达集】校验——5 门必须【全部存在】,缺任一即判失败。
// 旧"遍历出现的门、出现才查"会被 harness 中途崩钻空子误判 PASS:play.cdp.cjs 的 runDriver(含 seek-food)在 // 旧"遍历出现的门、出现才查"会被 harness 中途崩钻空子误判 PASS:play.cdp.cjs 的 runDriver(含 seek-food)在
// D_render/E_live 之前执行且未 try/catch,conc=12 下 CDP 抖动抛错→主 catch 只补 A_boot→verdict 只剩 // D_render/E_live 之前执行且未 try/catch,conc=12 下 CDP 抖动抛错→主 catch 只补 A_boot→verdict 只剩
// A_boot/C_frame/I_control→旧逻辑 sawObjective=true 放行=【崩掉、根本没测到渲染/活性的游戏被判过】。 // A_boot/C_frame/I_control→旧逻辑 sawObjective=true 放行=【崩掉、根本没测到渲染/活性的游戏被判过】。
// 缺门=harness 未跑完=失败,才安全(旧 rc==0 口径反不会误判;本修补回该安全性、又不重新绑 driver 门)。 // 缺门=harness 未跑完=失败,才安全。advisory 门要求【存在】(保住该崩溃安全)但【允许 pass=false】(未驱动无法公平验活性)。
for (String gate : OBJECTIVE_GATES) { for (String gate : OBJECTIVE_GATES) {
JsonNode g = guards.path(gate); JsonNode g = guards.path(gate);
if (g.isMissingNode() || !g.path("pass").asBoolean(false)) { if (g.isMissingNode()) {
return false; // 客观门缺失或未过 → 生成硬门判失败。 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; return true;
} }
/** 读 verdict.advisoryGates(play.cdp.cjs 在未驱动时降级的非致命门,如 E_live/H_progress)。缺/非数组→空集。 */
private static java.util.Set<String> readAdvisoryGates(JsonNode verdict) {
java.util.Set<String> 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)。 * 把失败的守卫摘成回喂文字(对齐 run.py:_verdict_feedback)。
*/ */
@ -742,6 +768,7 @@ final class SaaGenNodes {
return "真玩阶段无 verdict 产出(疑似装载即崩/超时)。"; return "真玩阶段无 verdict 产出(疑似装载即崩/超时)。";
} }
List<String> lines = new ArrayList<>(); List<String> lines = new ArrayList<>();
java.util.Set<String> advisory = readAdvisoryGates(verdict);
JsonNode guards = verdict.path("guards"); JsonNode guards = verdict.path("guards");
if (guards.isObject()) { if (guards.isObject()) {
Iterator<Map.Entry<String, JsonNode>> it = guards.fields(); Iterator<Map.Entry<String, JsonNode>> it = guards.fields();
@ -751,6 +778,10 @@ final class SaaGenNodes {
if (!GATE_ALL9 && !OBJECTIVE_GATES.contains(e.getKey())) { if (!GATE_ALL9 && !OBJECTIVE_GATES.contains(e.getKey())) {
continue; continue;
} }
// B 收尾:advisory 门(未驱动 E_live/H)非致命,不回喂救场——否则空喂"E_live 未过"驱动无谓重生成。
if (!GATE_ALL9 && advisory.contains(e.getKey())) {
continue;
}
if (!e.getValue().path("pass").asBoolean(false)) { if (!e.getValue().path("pass").asBoolean(false)) {
String g = e.getValue().toString(); String g = e.getValue().toString();
lines.add("- 守卫 " + e.getKey() + " 未过:" + (g.length() > 200 ? g.substring(0, 200) : g)); lines.add("- 守卫 " + e.getKey() + " 未过:" + (g.length() > 200 ? g.substring(0, 200) : g));

View File

@ -1,5 +1,7 @@
package com.wanxiang.huijing.game.module.aigc.saa; 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 com.wanxiang.huijing.game.module.aigc.service.executor.AigcExecutorProperties;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -246,4 +248,71 @@ class SaaFullGraphE2eGateLogicTest {
assertEquals(1, clamp.size(), "k<1 → 至少 1 槽(防御,不返空)"); assertEquals(1, clamp.size(), "k<1 → 至少 1 槽(防御,不返空)");
assertEquals(4320, clamp.get(0)[0]); 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 不应回喂救场");
}
} }