将策划决策轮次交由Runtime管理
移除Provider输入中的 decisions.round 字段 由Runtime按决策顺序生成持久化轮次 放宽GDD实际门禁并同步模型数量上限
This commit is contained in:
+23
-26
@@ -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": "多数玩家完成目标"}
|
||||
|
||||
+36
-52
@@ -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 必须有 3~5 项"));
|
||||
if !(1..=6).contains(&game.art_style.keywords.len()) {
|
||||
return Err(invalid("game.artStyle.keywords 必须有 1~6 项"));
|
||||
}
|
||||
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 必须有 2~4 条"));
|
||||
if !(1..=6).contains(&game.pillars.len()) {
|
||||
return Err(invalid("game.pillars 必须有 1~6 条"));
|
||||
}
|
||||
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 必须有 4~8 步"));
|
||||
if !(1..=8).contains(&game.core_loop.len()) {
|
||||
return Err(invalid("game.coreLoop 必须有 1~8 步"));
|
||||
}
|
||||
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 必须有 3~6 项"));
|
||||
if !(1..=8).contains(&game.mvp_systems.len()) {
|
||||
return Err(invalid("game.mvpSystems 必须有 1~8 项"));
|
||||
}
|
||||
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 必须有 1~12 项"));
|
||||
if game.out_of_scope.len() > 12 {
|
||||
return Err(invalid("game.outOfScope 必须有 0~12 项"));
|
||||
}
|
||||
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)?;
|
||||
|
||||
Reference in New Issue
Block a user