调整策划 V2 决定 ID 由 Runtime 分配
删除 plan_submit_gdd Provider schema 中的 decisions[].id 和 prototypeValidationItems[].id 由 Runtime 生成 initial-request 与后续决定 ID,并绑定原型验证项 同步更新 Planning V2 技术方案和项目决策记录
This commit is contained in:
+184
-98
@@ -36,7 +36,6 @@ pub(crate) struct PlanningQuestionOptionV2 {
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct PlanningGddDecisionInputV2 {
|
||||
pub id: String,
|
||||
pub topic: String,
|
||||
pub state: String,
|
||||
#[serde(default)]
|
||||
@@ -45,6 +44,15 @@ pub(crate) struct PlanningGddDecisionInputV2 {
|
||||
pub answer_summary: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct PlanningPrototypeValidationItemInputV2 {
|
||||
pub question: String,
|
||||
pub micro_prototype: String,
|
||||
pub observation: String,
|
||||
pub pass_criterion: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct PlanningGddInputV2 {
|
||||
@@ -52,7 +60,7 @@ pub(crate) struct PlanningGddInputV2 {
|
||||
pub schema_version: Option<String>,
|
||||
pub game: PlanSubmitGame,
|
||||
pub decisions: Vec<PlanningGddDecisionInputV2>,
|
||||
pub prototype_validation_items: Vec<PlanPrototypeValidationItem>,
|
||||
pub prototype_validation_items: Vec<PlanningPrototypeValidationItemInputV2>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
@@ -359,7 +367,6 @@ pub(crate) fn planning_v2_function_tools() -> Vec<platform_llm::LlmFunctionTool>
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "决定 id"},
|
||||
"topic": {"type": "string", "description": "主题"},
|
||||
"state": decision_state.clone(),
|
||||
"answerSource": {
|
||||
@@ -373,23 +380,23 @@ pub(crate) fn planning_v2_function_tools() -> Vec<platform_llm::LlmFunctionTool>
|
||||
},
|
||||
"answerSummary": {"type": "string", "description": "结论摘要"}
|
||||
},
|
||||
"required": ["id", "topic", "state", "round", "answerSummary"]
|
||||
"description": "决定内容",
|
||||
"required": ["topic", "state", "round", "answerSummary"]
|
||||
}
|
||||
},
|
||||
"prototypeValidationItems": {
|
||||
"type": "array",
|
||||
"description": "无 prototype_pending 时为 [];有则按决定 id 对应填写",
|
||||
"description": "无 prototype_pending 时为 [];有则按 decisions 中 prototype_pending 决定的顺序填写",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "与待验证决定对应的 id"},
|
||||
"question": {"type": "string", "description": "要验证的问题"},
|
||||
"microPrototype": {"type": "string", "description": "最小原型做法"},
|
||||
"observation": {"type": "string", "description": "观察什么"},
|
||||
"passCriterion": {"type": "string", "description": "通过标准"}
|
||||
},
|
||||
"required": ["id", "question", "microPrototype", "observation", "passCriterion"]
|
||||
"required": ["question", "microPrototype", "observation", "passCriterion"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -584,101 +591,138 @@ fn validate_v2_game(game: &PlanGddGame) -> Result<(), String> {
|
||||
.map_err(|error| format!("PLANNING_INVALID_GDD: {error}"))
|
||||
}
|
||||
|
||||
fn validate_v2_decisions(
|
||||
fn validate_v2_decision_content(
|
||||
index: usize,
|
||||
topic: &str,
|
||||
state: &str,
|
||||
answer_source: Option<String>,
|
||||
round: u32,
|
||||
answer_summary: &str,
|
||||
) -> Result<(), String> {
|
||||
validate_text(topic, &format!("decisions[{index}].topic"), 1, 120)
|
||||
.map_err(|error| error.to_string())?;
|
||||
normalize_v2_decision_state(state)?;
|
||||
normalize_answer_source(state, answer_source)?;
|
||||
validate_text(
|
||||
answer_summary,
|
||||
&format!("decisions[{index}].answerSummary"),
|
||||
1,
|
||||
if index == 0 {
|
||||
PLAN_INITIAL_REQUEST_MAX_CHARS
|
||||
} else {
|
||||
PLAN_DECISION_ANSWER_SUMMARY_MAX_CHARS
|
||||
},
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if index == 0 && (normalize_v2_decision_state(state)? != "confirmed" || round != 0) {
|
||||
return Err("PLANNING_INVALID_GDD: decisions 首项必须是 confirmed 且 round=0".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_v2_prototype_content(
|
||||
question: &str,
|
||||
micro_prototype: &str,
|
||||
observation: &str,
|
||||
pass_criterion: &str,
|
||||
) -> Result<(), String> {
|
||||
for (label, value) in [
|
||||
("question", question),
|
||||
("microPrototype", micro_prototype),
|
||||
("observation", observation),
|
||||
("passCriterion", pass_criterion),
|
||||
] {
|
||||
validate_text(value, &format!("prototypeValidationItems.{label}"), 1, 400)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_v2_decision_inputs(
|
||||
decisions: &[PlanningGddDecisionInputV2],
|
||||
prototype_items: &[PlanPrototypeValidationItem],
|
||||
prototype_items: &[PlanningPrototypeValidationItemInputV2],
|
||||
) -> Result<(), String> {
|
||||
if !(1..=64).contains(&decisions.len()) {
|
||||
return Err("PLANNING_INVALID_GDD: decisions 必须有 1~64 项".to_string());
|
||||
}
|
||||
let mut ids = std::collections::BTreeSet::new();
|
||||
for (index, decision) in decisions.iter().enumerate() {
|
||||
validate_v2_decision_content(
|
||||
index,
|
||||
&decision.topic,
|
||||
&decision.state,
|
||||
decision.answer_source.clone(),
|
||||
decision.round,
|
||||
&decision.answer_summary,
|
||||
)?;
|
||||
}
|
||||
let prototype_count = decisions
|
||||
.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());
|
||||
}
|
||||
for item in prototype_items {
|
||||
validate_v2_prototype_content(
|
||||
&item.question,
|
||||
&item.micro_prototype,
|
||||
&item.observation,
|
||||
&item.pass_criterion,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_v2_durable_decisions(
|
||||
decisions: &[PlanDecision],
|
||||
prototype_items: &[PlanPrototypeValidationItem],
|
||||
) -> Result<(), String> {
|
||||
// Provider 输入不携带 ID;这里只校验 Runtime 已生成或已持久化产物的
|
||||
// 内部引用完整性,避免把模型负责的内容校验与产物身份校验混在一起。
|
||||
if !(1..=64).contains(&decisions.len()) {
|
||||
return Err("PLANNING_INVALID_GDD: decisions 必须有 1~64 项".to_string());
|
||||
}
|
||||
|
||||
let mut prototype_ids = std::collections::BTreeSet::new();
|
||||
for (index, decision) in decisions.iter().enumerate() {
|
||||
if decision.id != "initial-request"
|
||||
&& (decision.id.len() > 64
|
||||
|| !decision.id.chars().enumerate().all(|(position, value)| {
|
||||
(position == 0 && value.is_ascii_lowercase())
|
||||
|| (position > 0
|
||||
&& (value.is_ascii_lowercase()
|
||||
|| value.is_ascii_digit()
|
||||
|| value == '-'))
|
||||
}))
|
||||
{
|
||||
return Err(format!(
|
||||
"PLANNING_INVALID_GDD: decisions[{index}].id 必须是 kebab-case"
|
||||
));
|
||||
}
|
||||
validate_text(&decision.id, &format!("decisions[{index}].id"), 1, 64)
|
||||
.map_err(|error| error.to_string())?;
|
||||
validate_text(
|
||||
validate_v2_decision_content(
|
||||
index,
|
||||
&decision.topic,
|
||||
&format!("decisions[{index}].topic"),
|
||||
1,
|
||||
120,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
normalize_v2_decision_state(&decision.state)?;
|
||||
normalize_answer_source(&decision.state, decision.answer_source.clone())?;
|
||||
validate_text(
|
||||
&decision.state,
|
||||
Some(decision.answer_source.clone()),
|
||||
decision.round,
|
||||
&decision.answer_summary,
|
||||
&format!("decisions[{index}].answerSummary"),
|
||||
1,
|
||||
if decision.id == "initial-request" {
|
||||
PLAN_INITIAL_REQUEST_MAX_CHARS
|
||||
} else {
|
||||
PLAN_DECISION_ANSWER_SUMMARY_MAX_CHARS
|
||||
},
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if !ids.insert(decision.id.as_str()) {
|
||||
return Err(format!(
|
||||
"PLANNING_INVALID_GDD: decisions.id 不能重复:{}",
|
||||
decision.id
|
||||
));
|
||||
}
|
||||
if normalize_v2_decision_state(&decision.state)? == "prototype_pending" {
|
||||
)?;
|
||||
if decision.state == "prototype_pending" {
|
||||
prototype_ids.insert(decision.id.as_str());
|
||||
}
|
||||
}
|
||||
if decisions.first().map(|value| value.id.as_str()) != Some("initial-request") {
|
||||
return Err("PLANNING_INVALID_GDD: decisions 第一项必须是 initial-request".to_string());
|
||||
}
|
||||
let first = &decisions[0];
|
||||
if first.state != "confirmed" || first.round != 0 {
|
||||
return Err("PLANNING_INVALID_GDD: initial-request 必须 confirmed 且 round=0".to_string());
|
||||
}
|
||||
if prototype_items.len() > 3 || prototype_ids.len() != prototype_items.len() {
|
||||
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());
|
||||
}
|
||||
let mut item_ids = std::collections::BTreeSet::new();
|
||||
for item in prototype_items {
|
||||
if !item_ids.insert(item.id.as_str()) || !prototype_ids.contains(item.id.as_str()) {
|
||||
return Err("PLANNING_INVALID_GDD: prototypeValidationItems.id 必须与 prototype_pending 决定双射".to_string());
|
||||
}
|
||||
if !item.id.chars().enumerate().all(|(position, value)| {
|
||||
(position == 0 && value.is_ascii_lowercase())
|
||||
|| (position > 0
|
||||
&& (value.is_ascii_lowercase() || value.is_ascii_digit() || value == '-'))
|
||||
}) {
|
||||
return Err(
|
||||
"PLANNING_INVALID_GDD: prototypeValidationItems.id 必须是 kebab-case".to_string(),
|
||||
);
|
||||
}
|
||||
validate_text(&item.id, "prototypeValidationItems.id", 1, 64)
|
||||
.map_err(|error| error.to_string())?;
|
||||
for (label, value) in [
|
||||
("question", &item.question),
|
||||
("microPrototype", &item.micro_prototype),
|
||||
("observation", &item.observation),
|
||||
("passCriterion", &item.pass_criterion),
|
||||
] {
|
||||
validate_text(value, &format!("prototypeValidationItems.{label}"), 1, 400)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
validate_v2_prototype_content(
|
||||
&item.question,
|
||||
&item.micro_prototype,
|
||||
&item.observation,
|
||||
&item.pass_criterion,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn runtime_decision_id(index: usize) -> String {
|
||||
if index == 0 {
|
||||
"initial-request".to_string()
|
||||
} else {
|
||||
format!("decision-{index}")
|
||||
}
|
||||
}
|
||||
|
||||
fn build_gdd_v2(
|
||||
project_id: &str,
|
||||
version: u32,
|
||||
@@ -690,9 +734,10 @@ fn build_gdd_v2(
|
||||
let decisions = input
|
||||
.decisions
|
||||
.into_iter()
|
||||
.map(|value| {
|
||||
.enumerate()
|
||||
.map(|(index, value)| {
|
||||
Ok(PlanDecision {
|
||||
id: value.id,
|
||||
id: runtime_decision_id(index),
|
||||
topic: value.topic,
|
||||
state: normalize_v2_decision_state(&value.state)?,
|
||||
answer_source: normalize_answer_source(&value.state, value.answer_source)?,
|
||||
@@ -702,6 +747,23 @@ fn build_gdd_v2(
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
let prototype_ids = decisions
|
||||
.iter()
|
||||
.filter(|decision| decision.state == "prototype_pending")
|
||||
.map(|decision| decision.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let prototype_validation_items = input
|
||||
.prototype_validation_items
|
||||
.into_iter()
|
||||
.zip(prototype_ids)
|
||||
.map(|(value, id)| PlanPrototypeValidationItem {
|
||||
id,
|
||||
question: value.question,
|
||||
micro_prototype: value.micro_prototype,
|
||||
observation: value.observation,
|
||||
pass_criterion: value.pass_criterion,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut gdd = PlanningGddV2 {
|
||||
schema_version: PLAN_GDD_V2_SCHEMA_VERSION.to_string(),
|
||||
project_id: project_id.to_string(),
|
||||
@@ -710,7 +772,7 @@ fn build_gdd_v2(
|
||||
created_at_utc: current_plan_timestamp_utc(),
|
||||
game,
|
||||
decisions,
|
||||
prototype_validation_items: input.prototype_validation_items,
|
||||
prototype_validation_items,
|
||||
fingerprint: String::new(),
|
||||
};
|
||||
gdd.fingerprint = fingerprint_gdd_v2(&gdd)?;
|
||||
@@ -723,7 +785,7 @@ pub(crate) fn validate_planning_policy_output_v2(
|
||||
match output {
|
||||
PlanningPolicyOutputV2::Question(question) => validate_question_v2(question),
|
||||
PlanningPolicyOutputV2::Gdd(input) => {
|
||||
validate_v2_decisions(&input.decisions, &input.prototype_validation_items)?;
|
||||
validate_v2_decision_inputs(&input.decisions, &input.prototype_validation_items)?;
|
||||
let game = planning_gdd_game_from_input(&input.game)?;
|
||||
validate_v2_game(&game)
|
||||
}
|
||||
@@ -758,19 +820,7 @@ fn validate_gdd_v2(value: &PlanningGddV2, project_id: &str) -> Result<(), String
|
||||
validate_uuid_prefixed(&value.gdd_id, "gdd-", "gddId").map_err(|error| error.to_string())?;
|
||||
validate_timestamp(&value.created_at_utc, "createdAtUtc").map_err(|error| error.to_string())?;
|
||||
validate_v2_game(&value.game)?;
|
||||
let decisions = value
|
||||
.decisions
|
||||
.iter()
|
||||
.map(|decision| PlanningGddDecisionInputV2 {
|
||||
id: decision.id.clone(),
|
||||
topic: decision.topic.clone(),
|
||||
state: decision.state.clone(),
|
||||
answer_source: Some(decision.answer_source.clone()),
|
||||
round: decision.round,
|
||||
answer_summary: decision.answer_summary.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
validate_v2_decisions(&decisions, &value.prototype_validation_items)?;
|
||||
validate_v2_durable_decisions(&value.decisions, &value.prototype_validation_items)?;
|
||||
let expected = fingerprint_gdd_v2(value)?;
|
||||
if expected != value.fingerprint {
|
||||
return Err("PLANNING_INVALID_GDD: GDD fingerprint 不匹配".to_string());
|
||||
@@ -1575,7 +1625,7 @@ mod tests {
|
||||
"outOfScope": ["多人联机"],
|
||||
"creatorTips": {"doFirst": "先做一张可走完的地图", "deferForNow": "暂缓复杂成长线", "howToVerify": "观察玩家是否能说出选择后果", "expandWhen": "连续三局都能理解后再扩展"}
|
||||
},
|
||||
"decisions": [{"id": "initial-request", "topic": "初始需求", "state": "confirmed", "round": 0, "answerSummary": "做一个短局守夜策略游戏"}],
|
||||
"decisions": [{"topic": "初始需求", "state": "confirmed", "round": 0, "answerSummary": "做一个短局守夜策略游戏"}],
|
||||
"prototypeValidationItems": []
|
||||
})
|
||||
}
|
||||
@@ -1649,12 +1699,48 @@ mod tests {
|
||||
assert!(gdd_properties.contains_key("game"));
|
||||
assert!(gdd_properties.contains_key("decisions"));
|
||||
assert!(!gdd_properties.contains_key("schemaVersion"));
|
||||
let decision_properties = gdd_properties["decisions"]["items"]["properties"]
|
||||
.as_object()
|
||||
.expect("decision properties");
|
||||
assert!(!decision_properties.contains_key("id"));
|
||||
assert!(!gdd_properties["decisions"]["items"]["required"]
|
||||
.as_array()
|
||||
.expect("decision required")
|
||||
.iter()
|
||||
.any(|value| value == "id"));
|
||||
let prototype_properties = gdd_properties["prototypeValidationItems"]["items"]
|
||||
["properties"]
|
||||
.as_object()
|
||||
.expect("prototype properties");
|
||||
assert!(!prototype_properties.contains_key("id"));
|
||||
assert!(gdd_properties["game"]["properties"]
|
||||
.as_object()
|
||||
.expect("game properties")
|
||||
.contains_key("title"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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": "验证核心回路"}
|
||||
]);
|
||||
value["prototypeValidationItems"] = serde_json::json!([
|
||||
{"question": "玩家是否理解核心回路?", "microPrototype": "做一个最小交互原型", "observation": "观察玩家行为", "passCriterion": "多数玩家完成目标"}
|
||||
]);
|
||||
let input: PlanningGddInputV2 = serde_json::from_value(value).expect("input");
|
||||
let gdd = build_gdd_v2("project-v2", 1, input).expect("build gdd");
|
||||
assert_eq!(
|
||||
gdd.decisions
|
||||
.iter()
|
||||
.map(|decision| decision.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["initial-request", "decision-1"]
|
||||
);
|
||||
assert_eq!(gdd.prototype_validation_items[0].id, "decision-1");
|
||||
}
|
||||
|
||||
fn sample_gdd_input() -> PlanningGddInputV2 {
|
||||
serde_json::from_value(sample_gdd_value()).expect("sample gdd input")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
- 关联文档:相关 PRD、技术文档、提交或 Issue
|
||||
```
|
||||
|
||||
## 2026-09-04 Planning V2 将决定 ID 从 Provider 输入移回 Runtime
|
||||
|
||||
- 背景:`plan_submit_gdd` 原先要求模型生成 `decisions[].id` 及原型验证项引用 ID。该字段既不是方案内容,又容易出现 `initial_request`、`initialRequest` 或错误层级,导致合法 GDD 在 Runtime 事后校验阶段失败。
|
||||
- 决策:Provider-facing `plan_submit_gdd` schema 和入参删除决定/原型验证项 ID。Runtime 按决定数组顺序生成首项 `initial-request`、后续 `decision-{序号}`,并按 `prototype_pending` 决定顺序给原型验证项绑定同一 ID。最终持久化 `plan-gdd.v2` 仍保留 ID,供审批、引用和 fingerprint 使用。V2 尚未上线,不为旧 Provider 输入或历史 V2 artifact 增加兼容转换;不符合新契约的历史数据按现有失败策略处理。
|
||||
- 影响范围:`planning_policy_v2.rs` 的工具 schema、Provider 入参解析、Runtime 产物构建与定向测试;Planning V2 技术方案。
|
||||
- 验证方式:定向 Rust 测试确认 schema 不含 ID、无 ID 输入可生成 Runtime ID;并运行 `cargo fmt --check`、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs`。
|
||||
|
||||
## 2026-09-04 Planning V2 把 `gdd.vN.json` 创建成功当作提交点
|
||||
|
||||
- 背景:V2 persist 先 create-only 写入不可变 GDD,再更新 index、Markdown、conversation 和 session。后续任一步失败会把 session 标成 `provider_failed`,但不回滚已创建文件;重试会重新生成 UUID/时间戳并撞上“已存在且内容不同”,hydrate 又只信 `current_artifact_version`,项目会卡死。
|
||||
|
||||
@@ -720,7 +720,7 @@ hydrate_planning_session_v2
|
||||
- 对已存在 V2 Session 的项目,打开项目时先 hydrate V2;没有 V2 authority 的旧项目继续走旧读取路径,避免误把旧项目数据当成 V2。
|
||||
- P3 已完成;旧会话 `legacy_retired` 封存、入口彻底关闭和真实 Provider/UI 全链路回归仍属于 P4/P5。
|
||||
|
||||
P3 之后的协议修正:V2 不再用正文 JSON 输出问询/GDD;Provider 请求挂 `plan_ask_question` / `plan_submit_gdd`,`tool_choice=auto`,形状由工具 schema 承担。system prompt 只保留三项核心闭环等策略和当前问询进度;数量、字数和 `initial-request` 仍由既有校验器在失败时回灌。入参不必回声 `schemaVersion`,落盘 GDD 仍写 `plan-gdd.v2`。失败结果不重复渲染,严格解析和失败不落盘成功产物的规则保持不变。
|
||||
P3 之后的协议修正:V2 不再用正文 JSON 输出问询/GDD;Provider 请求挂 `plan_ask_question` / `plan_submit_gdd`,`tool_choice=auto`,形状由工具 schema 承担。system prompt 只保留三项核心闭环等策略和当前问询进度;数量和字数由既有校验器在失败时回灌。`plan_submit_gdd` 的工具参数只包含决定和原型验证内容,不包含任何 Runtime 分配的 ID;Runtime 在落盘前为决定分配首项 `initial-request`、后续 `decision-{序号}`,并按 `prototype_pending` 顺序绑定原型验证项。入参不必回声 `schemaVersion`,落盘 GDD 仍写 `plan-gdd.v2`。失败结果不重复渲染,严格解析和失败不落盘成功产物的规则保持不变。历史 V2 数据不做兼容转换,按现有恢复/失败策略处理。
|
||||
|
||||
### P4:灰度、真实 Provider 与回归验收
|
||||
|
||||
|
||||
Reference in New Issue
Block a user