将策划决策轮次交由Runtime管理
Project CI / Repository checks (pull_request) Successful in 4m24s
Project CI / Frontend tests (pull_request) Successful in 20m53s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Failing after 13m28s

移除Provider输入中的 decisions.round 字段

由Runtime按决策顺序生成持久化轮次

放宽GDD实际门禁并同步模型数量上限
This commit is contained in:
2026-09-05 06:07:32 +00:00
parent 250360a40a
commit eabd8f393a
2 changed files with 59 additions and 78 deletions
@@ -40,7 +40,6 @@ pub(crate) struct PlanningGddDecisionInputV2 {
pub state: String,
#[serde(default)]
pub answer_source: Option<String>,
pub round: u32,
pub answer_summary: String,
}
@@ -252,7 +251,7 @@ pub(crate) fn planning_v2_function_tools() -> Vec<platform_llm::LlmFunctionTool>
"keywords": {
"type": "array",
"description": "风格关键词",
"minItems": 3,
"minItems": 1,
"maxItems": 5,
"items": {"type": "string"}
},
@@ -265,8 +264,8 @@ pub(crate) fn planning_v2_function_tools() -> Vec<platform_llm::LlmFunctionTool>
"pillars": {
"type": "array",
"description": "游戏支柱",
"minItems": 2,
"maxItems": 4,
"minItems": 1,
"maxItems": 5,
"items": {
"type": "object",
"additionalProperties": false,
@@ -282,8 +281,8 @@ pub(crate) fn planning_v2_function_tools() -> Vec<platform_llm::LlmFunctionTool>
"coreLoop": {
"type": "array",
"description": "单局核心循环步骤",
"minItems": 4,
"maxItems": 8,
"minItems": 1,
"maxItems": 7,
"items": {"type": "string"}
},
"targetUsers": {
@@ -305,8 +304,8 @@ pub(crate) fn planning_v2_function_tools() -> Vec<platform_llm::LlmFunctionTool>
"mvpSystems": {
"type": "array",
"description": "MVP 必须有的系统",
"minItems": 3,
"maxItems": 6,
"minItems": 1,
"maxItems": 7,
"items": {
"type": "object",
"additionalProperties": false,
@@ -329,8 +328,8 @@ pub(crate) fn planning_v2_function_tools() -> Vec<platform_llm::LlmFunctionTool>
"outOfScope": {
"type": "array",
"description": "暂不做的内容",
"minItems": 1,
"maxItems": 12,
"minItems": 0,
"maxItems": 10,
"items": {"type": "string"}
},
"creatorTips": {
@@ -373,15 +372,10 @@ pub(crate) fn planning_v2_function_tools() -> Vec<platform_llm::LlmFunctionTool>
"type": "string",
"description": "来源标记,如 user_freeform、user_choice、agent_inferred"
},
"round": {
"type": "integer",
"minimum": 0,
"description": "对应回合,初始需求为 0"
},
"answerSummary": {"type": "string", "description": "结论摘要"}
},
"description": "决定内容",
"required": ["topic", "state", "round", "answerSummary"]
"required": ["topic", "state", "answerSummary"]
}
},
"prototypeValidationItems": {
@@ -598,7 +592,7 @@ fn validate_v2_decision_content(
answer_source: Option<String>,
answer_summary: &str,
) -> Result<(), String> {
validate_text(topic, &format!("decisions[{index}].topic"), 1, 120)
validate_text(topic, &format!("decisions[{index}].topic"), 1, 400)
.map_err(|error| error.to_string())?;
normalize_v2_decision_state(state)?;
normalize_answer_source(state, answer_source)?;
@@ -628,7 +622,7 @@ fn validate_v2_prototype_content(
("observation", observation),
("passCriterion", pass_criterion),
] {
validate_text(value, &format!("prototypeValidationItems.{label}"), 1, 400)
validate_text(value, &format!("prototypeValidationItems.{label}"), 1, 800)
.map_err(|error| error.to_string())?;
}
Ok(())
@@ -654,8 +648,8 @@ fn validate_v2_decision_inputs(
.iter()
.filter(|decision| decision.state.trim() == "prototype_pending")
.count();
if prototype_items.len() > 3 || prototype_count != prototype_items.len() {
return Err("PLANNING_INVALID_GDD: prototypeValidationItems 必须逐项对应 prototype_pending 决定且最多 3".to_string());
if prototype_items.len() > 16 || prototype_count != prototype_items.len() {
return Err("PLANNING_INVALID_GDD: prototypeValidationItems 必须逐项对应 prototype_pending 决定且最多 16".to_string());
}
for item in prototype_items {
validate_v2_prototype_content(
@@ -692,8 +686,8 @@ fn validate_v2_durable_decisions(
}
}
let mut item_ids = std::collections::BTreeSet::new();
if prototype_ids.len() != prototype_items.len() {
return Err("PLANNING_INVALID_GDD: prototypeValidationItems 必须逐项对应 prototype_pending 决定且最多 3".to_string());
if prototype_ids.len() != prototype_items.len() || prototype_items.len() > 16 {
return Err("PLANNING_INVALID_GDD: prototypeValidationItems 必须逐项对应 prototype_pending 决定且最多 16".to_string());
}
for item in prototype_items {
if !item_ids.insert(item.id.as_str()) || !prototype_ids.contains(item.id.as_str()) {
@@ -735,7 +729,9 @@ fn build_gdd_v2(
topic: value.topic,
state: normalize_v2_decision_state(&value.state)?,
answer_source: normalize_answer_source(&value.state, value.answer_source)?,
round: value.round,
// round 是 Runtime 元数据:首项代表初始需求,其余决策按
// 输出顺序归入当前最多 3 轮的澄清区间,不接受模型自行指定。
round: (index as u32).min(3),
answer_summary: value.answer_summary,
basis: None,
})
@@ -1619,7 +1615,7 @@ mod tests {
"outOfScope": ["多人联机"],
"creatorTips": {"doFirst": "先做一张可走完的地图", "deferForNow": "暂缓复杂成长线", "howToVerify": "观察玩家是否能说出选择后果", "expandWhen": "连续三局都能理解后再扩展"}
},
"decisions": [{"topic": "初始需求", "state": "confirmed", "round": 0, "answerSummary": "做一个短局守夜策略游戏"}],
"decisions": [{"topic": "初始需求", "state": "confirmed", "answerSummary": "做一个短局守夜策略游戏"}],
"prototypeValidationItems": []
})
}
@@ -1697,6 +1693,7 @@ mod tests {
.as_object()
.expect("decision properties");
assert!(!decision_properties.contains_key("id"));
assert!(!decision_properties.contains_key("round"));
assert!(!gdd_properties["decisions"]["items"]["required"]
.as_array()
.expect("decision required")
@@ -1717,8 +1714,8 @@ mod tests {
fn runtime_assigns_decision_and_prototype_ids() {
let mut value = sample_gdd_value();
value["decisions"] = serde_json::json!([
{"topic": "初始需求", "state": "confirmed", "round": 0, "answerSummary": "做一个短局守夜策略游戏"},
{"topic": "核心回路", "state": "prototype_pending", "round": 1, "answerSummary": "验证核心回路"}
{"topic": "初始需求", "state": "confirmed", "answerSummary": "做一个短局守夜策略游戏"},
{"topic": "核心回路", "state": "prototype_pending", "answerSummary": "验证核心回路"}
]);
value["prototypeValidationItems"] = serde_json::json!([
{"question": "玩家是否理解核心回路?", "microPrototype": "做一个最小交互原型", "observation": "观察玩家行为", "passCriterion": "多数玩家完成目标"}
@@ -118,7 +118,7 @@ pub(crate) const PLAN_FAST_GDD_PATH: &str = "game/fast_gdd.md";
pub(crate) const PLAN_FAST_GDD_MAX_BYTES: usize = 128 * 1024;
/// 单条决定 answerSummary 的上限(`initial-request` 除外)。
pub(crate) const PLAN_DECISION_ANSWER_SUMMARY_MAX_CHARS: usize = 400;
pub(crate) const PLAN_DECISION_ANSWER_SUMMARY_MAX_CHARS: usize = 800;
/// `initial-request` 那条决定的 answerSummary 上限,也就是立项策划入口原始需求的上限。
///
@@ -660,9 +660,9 @@ fn validate_plan_platform_facts(value: &PlanPlatformFacts) -> Result<(), Plannin
pub(crate) fn validate_plan_game(game: &PlanGddGame) -> Result<(), PlanningStorageError> {
validate_text(&game.title, "game.title", 1, 80)?;
validate_text(&game.genre.primary, "game.genre.primary", 1, 40)?;
validate_text(&game.genre.primary, "game.genre.primary", 1, 80)?;
if let Some(fusion) = &game.genre.fusion {
validate_text(fusion, "game.genre.fusion", 1, 40)?;
validate_text(fusion, "game.genre.fusion", 1, 80)?;
}
validate_text(
&game.art_style.visual_type,
@@ -670,52 +670,44 @@ pub(crate) fn validate_plan_game(game: &PlanGddGame) -> Result<(), PlanningStora
1,
80,
)?;
if !(3..=5).contains(&game.art_style.keywords.len()) {
return Err(invalid("game.artStyle.keywords 必须有 35"));
if !(1..=6).contains(&game.art_style.keywords.len()) {
return Err(invalid("game.artStyle.keywords 必须有 16"));
}
validate_unique(
game.art_style.keywords.iter().map(String::as_str),
"game.artStyle.keywords",
)?;
for (index, keyword) in game.art_style.keywords.iter().enumerate() {
validate_text(keyword, &format!("game.artStyle.keywords[{index}]"), 1, 32)?;
validate_text(keyword, &format!("game.artStyle.keywords[{index}]"), 1, 64)?;
}
validate_text(
&game.art_style.mood_and_color,
"game.artStyle.moodAndColor",
1,
400,
1000,
)?;
validate_text(
&game.art_style.mvp_art_boundary,
"game.artStyle.mvpArtBoundary",
1,
400,
1200,
)?;
// 设计约定:Runtime 接受范围为 10~160,模型 schema 提示范围收紧为 25~90
// 两者有意不对称,用于避免过短概念同时保留对已有/人工 GDD 的兼容,不是缺陷或 bug。
validate_text(&game.one_liner, "game.oneLiner", 10, 160)?;
if !(2..=4).contains(&game.pillars.len()) {
return Err(invalid("game.pillars 必须有 24"));
if !(1..=6).contains(&game.pillars.len()) {
return Err(invalid("game.pillars 必须有 16"));
}
validate_unique(
game.pillars.iter().map(|item| item.name.as_str()),
"game.pillars.name",
)?;
for (index, pillar) in game.pillars.iter().enumerate() {
validate_text(&pillar.name, &format!("game.pillars[{index}].name"), 1, 40)?;
validate_text(&pillar.name, &format!("game.pillars[{index}].name"), 1, 80)?;
validate_text(
&pillar.player_feel,
&format!("game.pillars[{index}].playerFeel"),
1,
240,
400,
)?;
validate_text(
&pillar.mechanism,
&format!("game.pillars[{index}].mechanism"),
1,
240,
400,
)?;
validate_decision_state(&pillar.decision_state)?;
if pillar.basis.is_some() {
@@ -723,33 +715,33 @@ pub(crate) fn validate_plan_game(game: &PlanGddGame) -> Result<(), PlanningStora
}
}
if !(4..=8).contains(&game.core_loop.len()) {
return Err(invalid("game.coreLoop 必须有 48 步"));
if !(1..=8).contains(&game.core_loop.len()) {
return Err(invalid("game.coreLoop 必须有 18 步"));
}
for (index, step) in game.core_loop.iter().enumerate() {
validate_text(step, &format!("game.coreLoop[{index}]"), 1, 120)?;
validate_text(step, &format!("game.coreLoop[{index}]"), 1, 400)?;
}
validate_text(
&game.target_users.core_users,
"game.targetUsers.coreUsers",
1,
240,
400,
)?;
validate_text(
&game.target_users.preferences,
"game.targetUsers.preferences",
1,
240,
400,
)?;
validate_text(
&game.target_users.session_length,
"game.targetUsers.sessionLength",
1,
240,
400,
)?;
if game.target_users.reference_games.len() > 5 {
return Err(invalid("game.targetUsers.referenceGames 最多 5"));
if game.target_users.reference_games.len() > 8 {
return Err(invalid("game.targetUsers.referenceGames 最多 8"));
}
for (index, reference) in game.target_users.reference_games.iter().enumerate() {
validate_text(
@@ -761,50 +753,42 @@ pub(crate) fn validate_plan_game(game: &PlanGddGame) -> Result<(), PlanningStora
}
validate_plan_platform_facts(&game.platform_facts)?;
if !(3..=6).contains(&game.mvp_systems.len()) {
return Err(invalid("game.mvpSystems 必须有 36"));
if !(1..=8).contains(&game.mvp_systems.len()) {
return Err(invalid("game.mvpSystems 必须有 18"));
}
validate_unique(
game.mvp_systems.iter().map(|item| item.system.as_str()),
"game.mvpSystems.system",
)?;
for (index, system) in game.mvp_systems.iter().enumerate() {
validate_text(
&system.system,
&format!("game.mvpSystems[{index}].system"),
1,
40,
80,
)?;
validate_text(
&system.minimal_function,
&format!("game.mvpSystems[{index}].minimalFunction"),
1,
240,
400,
)?;
validate_text(
&system.why_required,
&format!("game.mvpSystems[{index}].whyRequired"),
1,
240,
400,
)?;
validate_text(
&system.verify_method,
&format!("game.mvpSystems[{index}].verifyMethod"),
1,
240,
400,
)?;
validate_decision_state(&system.decision_state)?;
if system.basis.is_some() {
return Err(invalid("v1 的 mvpSystem.basis 必须为 null"));
}
}
if !(1..=12).contains(&game.out_of_scope.len()) {
return Err(invalid("game.outOfScope 必须有 112 项"));
if game.out_of_scope.len() > 12 {
return Err(invalid("game.outOfScope 必须有 012 项"));
}
validate_unique(
game.out_of_scope.iter().map(String::as_str),
"game.outOfScope",
)?;
for (index, item) in game.out_of_scope.iter().enumerate() {
validate_text(item, &format!("game.outOfScope[{index}]"), 1, 80)?;
}
@@ -812,25 +796,25 @@ pub(crate) fn validate_plan_game(game: &PlanGddGame) -> Result<(), PlanningStora
&game.creator_tips.do_first,
"game.creatorTips.doFirst",
1,
400,
1000,
)?;
validate_text(
&game.creator_tips.defer_for_now,
"game.creatorTips.deferForNow",
1,
400,
1000,
)?;
validate_text(
&game.creator_tips.how_to_verify,
"game.creatorTips.howToVerify",
1,
400,
1000,
)?;
validate_text(
&game.creator_tips.expand_when,
"game.creatorTips.expandWhen",
1,
400,
1000,
)?;
Ok(())
}
@@ -934,8 +918,8 @@ fn validate_decisions(
prototype_decisions.insert(decision.id.as_str());
}
}
if prototype_items.len() > 3 {
return Err(invalid("prototypeValidationItems 最多 3"));
if prototype_items.len() > 16 {
return Err(invalid("prototypeValidationItems 最多 16"));
}
validate_unique(
prototype_items.iter().map(|item| item.id.as_str()),
@@ -1954,7 +1938,7 @@ fn validate_plan_session_shape(value: &PlanSessionV1) -> Result<(), PlanningStor
&decision.topic,
&format!("session.decisionsSummary[{index}].topic"),
1,
80,
400,
)?;
validate_decision_state(&decision.state)?;
validate_answer_source(&decision.answer_source)?;