feat(saa): 零-LLM driver 内置绑定 injectDriverByArchetype + 5 单测

design 只声明 gatespec 不写 driver→amodel 路无人绑 driver→游戏停菜单→九门 E_live/H 名存实亡。按 classify archetype 确定性补内置 driver(经营/反应→tap occupied、三消→placement、跑酷→key-cycle),显式 driver 不覆盖、无合适内置(如 bubble)不强补。WIP:未在 mini mvn 验;archetype→targets 契约假设正于 driver 调查中复核。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-23 19:45:33 -07:00
parent f5918e449e
commit d342de088e
2 changed files with 97 additions and 0 deletions

View File

@ -457,6 +457,11 @@ final class SaaGenNodes {
try {
Path spec = gameRuntimeRoot.resolve("games").resolve("_wg1-gen").resolve(gameId).resolve("play-spec.json");
String playSpec = s.value(SaaStudioNodes.K_PLAY_SPEC, String.class).orElse("{}");
// 零-LLM driver 内置绑定:design 按 gatespec 规则③只声明「什么算赢/有进展」、不写 driver;amodel 路无人绑 driver →
// 游戏不被驱动→停菜单→九门 E_live/H 因「未驱动」降 advisory 名存实亡(创始人「门要可信」指令)。此处据 classify 的
// archetype 确定性补内置 driver,让经营/反应/放置类被真玩通,使门真正测到「能不能玩」。已显式声明 driver 则尊重不覆盖。
String archetype = s.value(SaaStudioNodes.K_ARCHETYPE, String.class).orElse("");
playSpec = injectDriverByArchetype(playSpec, archetype);
Files.write(spec, playSpec.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
errs.add("amodel play-spec.json 写入失败:" + e.getMessage());
@ -795,6 +800,54 @@ final class SaaGenNodes {
return lines.isEmpty() ? "真玩未过(原因未知)。" : "真玩未过:\n" + String.join("\n", lines);
}
/**
* 零-LLM driver 内置绑定:据 classify 的 archetype 确定性映射到 play.cdp.cjs 内置 driver(harness 自适应读 _forensicsView state 出招)。
* <p>design 按 gatespec 规则③只声明「什么算赢/有进展」、不写 driver;但 amodel 路无人绑 driver → 游戏不被驱动 → 停菜单 →
* 九门 E_live/H 因「未驱动」降 advisory 名存实亡。本处补齐:让经营/反应/放置等类被真玩通,使门可信(创始人指令)。
* <p><b>已含 driver(显式声明)则原样返回不覆盖</b>;无合适内置 driver(如 narrative)则不补;解析异常原样返回(绝不因补 driver 破坏 play-spec)。
*/
static String injectDriverByArchetype(String playSpecJson, String archetype) {
try {
JsonNode node = MAPPER.readTree((playSpecJson == null || playSpecJson.isBlank()) ? "{}" : playSpecJson);
if (!node.isObject()) {
return playSpecJson;
}
com.fasterxml.jackson.databind.node.ObjectNode spec = (com.fasterxml.jackson.databind.node.ObjectNode) node;
if (spec.has("driver") && !spec.get("driver").isNull()) {
return spec.toString(); // 显式 driver 优先,不覆盖
}
com.fasterxml.jackson.databind.node.ObjectNode driver = driverForArchetype(archetype);
if (driver == null) {
return spec.toString(); // 无合适内置 driver(如 narrative)→ 不补,保持未驱动(E_live/H 走 advisory)
}
spec.set("driver", driver);
return spec.toString();
} catch (Exception e) {
return playSpecJson; // 解析异常 → 原样,不破坏 play-spec
}
}
/** archetype → 内置 driver 配置(null=无合适内置 driver)。保守可扩展:经营/反应类点 occupied 目标,放置类点空位,跑酷/躲避走键循环。 */
private static com.fasterxml.jackson.databind.node.ObjectNode driverForArchetype(String archetype) {
String a = (archetype == null) ? "" : archetype.trim().toLowerCase();
com.fasterxml.jackson.databind.node.ObjectNode d = MAPPER.createObjectNode();
switch (a) {
// 经营/放置/合成/点击器/打地鼠/兜底:点「有内容」的离散目标(等待的客/冒头的鼠/可收产出)推进 → 服务族 tap-occupied。
case "tycoon": case "idle": case "clicker": case "merge": case "generic": case "":
return d.put("type", "tap-targets").put("targetsPath", "targets").put("targetMode", "occupied")
.put("steps", 80).put("stepMs", 500);
// 三消/消行:点离散空位/可消组 → 放置族(默认 occupied!==true)。
case "match3": case "line-clear":
return d.put("type", "tap-targets").put("targetsPath", "targets").put("steps", 40).put("stepMs", 360);
// 跑酷/躲避:连续输入 → 键循环兜底。
case "runner": case "dodge":
return d.put("type", "key-cycle").put("steps", 60).put("stepMs", 220);
// 瞄准发射(泡泡龙)等:无通用内置参数,本期不补 → 不驱动(E_live/H advisory),留未来 aim-fire 绑定。
default:
return null;
}
}
/** 取字符串尾部 n 字(日志摘要用)。 */
private static String tail(String s, int n) {
if (s == null) {

View File

@ -315,4 +315,48 @@ class SaaFullGraphE2eGateLogicTest {
assertTrue(fb.contains("D_render"), "真硬门 D_render 失败应回喂救场");
assertFalse(fb.contains("E_live"), "advisory E_live 不应回喂救场");
}
// ═══════════════════════ ② driver 内置绑定 archetype→driver(零-LLM,2026-06-23 wanglanmei E2E)═══════════════════════
//
// design 只声明 gatespec、不写 driver → amodel 路无人绑 driver → 游戏不被驱动 → 九门 E_live/H 名存实亡。
// injectDriverByArchetype 据 classify archetype 确定性补内置 driver,使经营/反应类被真玩通(创始人「门要可信」)。
@Test
@DisplayName("injectDriverByArchetype:tycoon(经营)→ 补 tap-targets occupied(点等待的客→真驱动)")
void injectDriver_tycoon_tapOccupied() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{\"expectLatch\":true}", "tycoon"));
assertEquals("tap-targets", v.path("driver").path("type").asText());
assertEquals("occupied", v.path("driver").path("targetMode").asText(), "经营/服务族点 occupied 目标");
assertTrue(v.path("expectLatch").asBoolean(), "原 gatespec 字段须保留");
}
@Test
@DisplayName("injectDriverByArchetype:match3 → tap-targets 放置族(不带 targetMode)")
void injectDriver_match3_placement() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{}", "match3"));
assertEquals("tap-targets", v.path("driver").path("type").asText());
assertTrue(v.path("driver").path("targetMode").isMissingNode(), "放置族不带 occupied 语义");
}
@Test
@DisplayName("injectDriverByArchetype:已显式 driver → 不覆盖(尊重显式声明)")
void injectDriver_respectsExplicit() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{\"driver\":{\"type\":\"seek-food\"}}", "tycoon"));
assertEquals("seek-food", v.path("driver").path("type").asText(), "显式 driver 优先,不被 archetype 覆盖");
}
@Test
@DisplayName("injectDriverByArchetype:无合适内置(bubble)→ 不补(保持未驱动 advisory)")
void injectDriver_noFit_skips() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{\"expectLatch\":true}", "bubble"));
assertTrue(v.path("driver").isMissingNode(), "无合适内置 driver 不强补(否则误绑→门误判)");
}
@Test
@DisplayName("injectDriverByArchetype:空/generic → 兜底 tap-targets occupied")
void injectDriver_genericFallback() {
JsonNode v = verdict(SaaGenNodes.injectDriverByArchetype("{}", ""));
assertEquals("tap-targets", v.path("driver").path("type").asText());
assertEquals("occupied", v.path("driver").path("targetMode").asText());
}
}