//! Plan-driven 规划骨架(Phase 0 · 维度 0.8 + 0.7) //! //! 论证依据:`docs/02-架构设计/单对话并行多轮-设计-2026-06-20.md`(§三 Plan 数据结构 / //! §十三 0.7 规划校验 / 0.8 planner.rs)+ //! `docs/02-架构设计/单对话并行多轮-Phase0落地路线图-2026-06-20.md`(0.8 + 0.7 节)。 //! //! ## 定位 //! **纯函数模块**(零 IO 零状态:无 `.await` / 无 db / 无 provider / 无全局状态), //! 仅数据结构 + 算法。便于单测 + Phase 1 无副作用接入。 //! //! 提供四项能力: //! 1. [`SubTask`] / [`Plan`] —— 规划 DAG 数据结构(可并行组 group + 顺序依赖 deps)。 //! 2. [`Plan::validate`] —— 五类非法形态校验(环/悬空/空/重复 id/超深)。 //! 3. [`Plan::to_layers`] —— Kahn 拓扑分层算法,输出可并行层(喂 Phase 2/3 execution 调度)。 //! 4. [`planning_enabled`] —— feature flag 占位(关时单链 ReAct = 旧行为,Phase 1 接入用)。 //! //! ## 设计原则(对齐设计文档 §十三 决策推翻) //! - **单一真相源**:Plan 只存 `deps`,**不冗余存 `parallel_groups`**(设计文档明确推翻 //! 旧「deps + parallel_groups 并存」结构 → 删 parallel_groups,运行时 Kahn 分层派生)。 //! `group` 字段是规划期的「提示性并行组」(可选 hint),与 deps DAG 并行不矛盾即合法。 //! - **非法收集非短路**:validate 一次性收集全部违规,便于规划 LLM 一次性修正。 //! - **Kahn 无副作用**:纯算法,不校验(validate 先行),输入非法时行为见各方法文档。 //! //! ## 不改现有文件 //! 本文件独立编译;`intent.rs` / `coordinator.rs` 接入留 Phase 1/2(plan_hint.rs 另起)。 // ---- feature flag 占位 ------------------------------------------------------ /// 规划能力总开关(主 loop 规划开关,门控 filter_tool_defs_planned 接入)。 /// /// **当前 `true`** = 主 loop 走 Plan 规划(agentic/mod.rs:633 调 filter_tool_defs_planned)。 /// Phase 1(plan_hint 接入主 loop)已落地,翻 true 启用。 /// 注:`PLAN_HINT_ENABLED`(plan_hint 函数 flag,Phase0a 就绪 true)单一定义在 `plan_hint.rs`, /// 本模块仅 `PLANNING_ENABLED`(主 loop 规划开关),消除此前双定义矛盾。 /// 双层开关:即使 PLANNING_ENABLED=true,若 plan_hint 内部 PLAN_HINT_ENABLED 关闭, /// filter_tool_defs_planned 仍退扁平(任一关闭退旧行为,可回退)。 pub const PLANNING_ENABLED: bool = true; /// Plan 层级硬上限(防爆深,防规划 LLM 退化成超长链)。 /// /// 超过此值的依赖链判 [`PlanError::TooDeep`]。 /// 8 层覆盖典型软件任务分解(需求→设计→编码→测试→…),Phase 2 go/no-go 后可调。 pub const MAX_PLAN_DEPTH: usize = 8; use std::collections::{HashMap, HashSet}; // ---- 类型别名 --------------------------------------------------------------- /// 子任务标识。String 而非裸 &str:规划期 LLM 自由生成,不依赖静态工具名表。 pub type SubTaskId = String; /// 并行组标识(可选 hint)。规划 LLM 标注「这组任务意图上可并行」, /// Kahn 分层不依赖它(只看 deps DAG),但保留供 UI/审计展示。 pub type GroupId = String; // ---- SubTask ---------------------------------------------------------------- /// 规划子任务:DAG 中的一个节点。 /// /// 字段对齐设计文档 §三 `SubTask`(id / tools / deps)+ 并行组 hint: /// - `id`:任务内唯一标识(validate 拒重复)。 /// - `tool_hint`:可用工具名子集(intent/plan_hint 提示,空 Vec 由 validate 拒为 /// [`PlanError::SubtaskWithoutTools`] —— 对齐设计文档枚举;执行期不被强制,仅提示 LLM)。 /// - `deps`:依赖子任务 id 列表(DAG 前驱)。未解析的 id(不存在于 Plan)→ /// [`PlanError::UnknownDep`](悬空)。 /// - `group`:可选并行组 hint(设计文档 §三 parallel_groups 的非冗余形态——只作提示, /// 不与 deps 并列做真相源)。 /// - `intent`:自由文本意图描述(规划 LLM 给出的「这步做什么」,便于审计/日志)。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct SubTask { pub id: SubTaskId, /// 可用工具名子集(hint,非强制)。空 → validate 拒(除非允许无工具子任务)。 pub tool_hint: Vec, /// 依赖前驱 id。DAG 边:dep → self。 pub deps: Vec, /// 并行组 hint(可选)。同 group 的任务意图上可并行,但 Kahn 不看它。 pub group: Option, /// 意图自由文本(规划 LLM 描述,审计/日志用)。 pub intent: String, } impl SubTask { /// 构造便捷器:最小子任务(id + intent,无工具无依赖)。 #[allow(dead_code)] pub fn new(id: impl Into, intent: impl Into) -> Self { Self { id: id.into(), tool_hint: Vec::new(), deps: Vec::new(), group: None, intent: intent.into(), } } /// 链式设置 tool_hint。 #[allow(dead_code)] pub fn with_tools(mut self, tools: Vec) -> Self { self.tool_hint = tools; self } /// 链式设置 deps。 #[allow(dead_code)] pub fn with_deps(mut self, deps: Vec) -> Self { self.deps = deps; self } /// 链式设置 group。 #[allow(dead_code)] pub fn with_group(mut self, group: impl Into) -> Self { self.group = Some(group.into()); self } } // ---- Plan ------------------------------------------------------------------- /// 规划 DAG:子任务集合。 /// /// **不冗余存 `parallel_groups`**(设计文档 §十三 决策推翻:删 parallel_groups, /// 只存 deps,运行时 [`Plan::to_layers`] Kahn 分层派生——单一真相源)。 /// /// 合法性由 [`Plan::validate`] 校验;非法 Plan 调 [`Plan::to_layers`] 行为见其文档。 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Plan { pub tasks: Vec, } impl Plan { /// 空构造。 #[allow(dead_code)] pub fn new() -> Self { Self { tasks: Vec::new() } } /// 从 tasks 构造。 #[allow(dead_code)] pub fn from_tasks(tasks: Vec) -> Self { Self { tasks } } /// 任务数。 #[allow(dead_code)] pub fn len(&self) -> usize { self.tasks.len() } /// 是否空(空 Plan 判非法:[`PlanError::Empty`])。 #[allow(dead_code)] pub fn is_empty(&self) -> bool { self.tasks.is_empty() } /// 校验 Plan 合法性(五类非法 + 设计文档四枚举)。 /// /// **收集非短路**:一次性返回所有违规(`Vec`),便于规划 LLM 一次修正。 /// 合法 → `Ok(())`。 /// /// 五类非法(对齐路线图 0.7)+ 设计文档枚举映射: /// | 类别 | 枚举 | 判据 | /// |---|---|---| /// | 空 | [`PlanError::Empty`] | tasks 为空 | /// | 重复 id | [`PlanError::DuplicateId`] | 同 id 出现 ≥2 次 | /// | 悬空 dep | [`PlanError::UnknownDep`] | deps 引用不存在的 id(设计文档枚举) | /// | 自环 dep | [`PlanError::UnknownDep`] 归类 | self-dep(A 依赖 A,视为悬空+环) | /// | 环 | [`PlanError::CycleDetected`] | deps 形成环(设计文档枚举) | /// | 无工具 | [`PlanError::SubtaskWithoutTools`] | tool_hint 空(设计文档枚举,可由 `require_tools=false` 关) | /// | 超深 | [`PlanError::TooDeep`] | 最长依赖链深度 > [`MAX_PLAN_DEPTH`] | /// /// 注:[`PlanError::OrphanSubtask`](设计文档枚举)当前归一不报—— /// 无 deps 且无后继的孤立任务在 DAG 中合法(入口任务),设计文档「Orphan」语义 /// 偏「脱网」,Phase 2 go/no-go 后再定是否拒(保留枚举占位,符合预留保留原则)。 pub fn validate(&self) -> Result<(), Vec> { let errs = self.validate_with(ValidateOptions::default()); if errs.is_empty() { Ok(()) } else { Err(errs) } } /// 带选项的校验(`require_tools` 开关:plan_hint 场景可允许无工具子任务)。 pub fn validate_with(&self, opts: ValidateOptions) -> Vec { let mut errs: Vec = Vec::new(); // 1) 空 if self.tasks.is_empty() { errs.push(PlanError::Empty); return errs; // 空 Plan 后续检查无意义 } // 构建 id 索引(同时检测重复 id) let mut id_index: HashMap<&SubTaskId, usize> = HashMap::new(); for (i, t) in self.tasks.iter().enumerate() { if t.id.is_empty() { errs.push(PlanError::EmptyId { index: i }); continue; } if id_index.contains_key(&t.id) { errs.push(PlanError::DuplicateId { id: t.id.clone(), }); } else { id_index.insert(&t.id, i); } } // 2) 无工具(可选) if opts.require_tools { for t in &self.tasks { if t.tool_hint.is_empty() { errs.push(PlanError::SubtaskWithoutTools { id: t.id.clone() }); } } } // 3) 悬空 dep / 自环(自环视为 UnknownDep) for t in &self.tasks { for dep in &t.deps { if dep == &t.id { errs.push(PlanError::SelfDep { id: t.id.clone() }); } else if !id_index.contains_key(dep) { errs.push(PlanError::UnknownDep { from: t.id.clone(), unknown: dep.clone(), }); } } } // 4) 环 + 超深(先验无悬空才能可靠跑环检测:悬空已报,环检测只看可解析边) if !errs.iter().any(|e| { matches!( e, PlanError::UnknownDep { .. } | PlanError::SelfDep { .. } ) }) { if let Some(cycle) = self.detect_cycle() { errs.push(PlanError::CycleDetected { cycle }); } // 超深(最长链 = Kahn 层数);环存在时层数无意义,跳过 if !errs.iter().any(|e| matches!(e, PlanError::CycleDetected { .. })) { if let Some(depth) = self.longest_chain_depth() { if depth > opts.max_depth { errs.push(PlanError::TooDeep { depth, max: opts.max_depth, }); } } } } errs } /// Kahn 拓扑分层:输出可并行层(同层任务无相互依赖,可并行执行)。 /// /// **算法**:入度 0 入当前层 → 移除(其后继入度 -1)→ 下层。纯算法无副作用。 /// /// **非法 Plan 行为**(调用方应先 validate): /// - 环:返回 `Err(CycleDetected)`(Kahn 无法消除环边,剩余节点 = 环成员)。 /// - 悬空 dep:忽略悬空边(视作无该前驱),照常分层。 /// - 空 Plan:返回 `Ok(vec![])`。 /// /// 返回层的顺序 = 执行顺序(第 0 层先执行,同层内可并行)。 /// 每层 `Vec` 为克隆(Kahn 消费入度,不消费原 Plan)。 pub fn to_layers(&self) -> Result>, CycleError> { if self.tasks.is_empty() { return Ok(Vec::new()); } let id_to_task: HashMap<&SubTaskId, &SubTask> = self.tasks.iter().map(|t| (&t.id, t)).collect(); // 入度:仅计可解析的 deps(悬空 dep 不计入,避免卡死) let mut indegree: HashMap<&SubTaskId, usize> = HashMap::new(); for t in &self.tasks { indegree.entry(&t.id).or_insert(0); } for t in &self.tasks { for dep in &t.deps { // dep 存在且非自环才计 if dep != &t.id && id_to_task.contains_key(dep) { *indegree.entry(&t.id).or_insert(0) += 1; } } } let mut layers: Vec> = Vec::new(); let mut placed: HashSet<&SubTaskId> = HashSet::new(); loop { // 当前层:入度 0 且未放置(按原 tasks 顺序稳定) let current: Vec<&SubTask> = self .tasks .iter() .filter(|t| !placed.contains(&t.id)) .filter(|t| *indegree.get(&t.id).unwrap_or(&0) == 0) .collect(); if current.is_empty() { break; } // 放置并减后继入度 for t in ¤t { placed.insert(&t.id); } // 后继入度 -1:遍历所有任务,若其 deps 含当前层任一 id,则 -1 let current_ids: HashSet<&SubTaskId> = current.iter().map(|t| &t.id).collect(); for t in &self.tasks { if placed.contains(&t.id) { continue; } for dep in &t.deps { if current_ids.contains(dep) { if let Some(v) = indegree.get_mut(&t.id) { *v = v.saturating_sub(1); } } } } layers.push(current.iter().map(|t| (*t).clone()).collect()); } // 剩余未放置 = 环成员 if placed.len() < self.tasks.len() { let cycle: Vec = self .tasks .iter() .filter(|t| !placed.contains(&t.id)) .map(|t| t.id.clone()) .collect(); return Err(CycleError { cycle }); } Ok(layers) } // ---- 内部算法辅助 ---- /// 环检测(DFS 三色标记法)。假设无悬空 dep(调用方已过滤)。 fn detect_cycle(&self) -> Option> { let id_to_task: HashMap<&SubTaskId, &SubTask> = self.tasks.iter().map(|t| (&t.id, t)).collect(); // 颜色:0=白(未访问) 1=灰(栈中) 2=黑(完成) let mut color: HashMap<&SubTaskId, u8> = HashMap::new(); for t in &self.tasks { color.insert(&t.id, 0); } // 记录路径用于回溯环 let mut stack: Vec<&SubTaskId> = Vec::new(); // finished 节点(防重复 DFS) let mut finished: HashSet<&SubTaskId> = HashSet::new(); for t in &self.tasks { if *color.get(&t.id).unwrap() != 0 { continue; } if let Some(cycle) = dfs_cycle(&t.id, &id_to_task, &mut color, &mut stack, &mut finished) { return Some(cycle); } } None } /// 最长依赖链深度(= Kahn 层数)。无环前提。返回 None 表示空 Plan。 fn longest_chain_depth(&self) -> Option { match self.to_layers() { Ok(layers) => { if layers.is_empty() { None } else { Some(layers.len()) } } // 环存在:返回 None(depth 无意义,由 CycleDetected 单独报) Err(_) => None, } } } // ---- validate 选项 ---------------------------------------------------------- /// validate 行为选项。 #[derive(Debug, Clone, Copy)] pub struct ValidateOptions { /// 是否要求每个子任务有 tool_hint。plan_hint 场景可关(允许纯编排节点)。 pub require_tools: bool, /// 依赖链深度上限。超 → TooDeep。 pub max_depth: usize, } impl Default for ValidateOptions { fn default() -> Self { Self { require_tools: true, max_depth: MAX_PLAN_DEPTH, } } } // ---- 错误类型 --------------------------------------------------------------- /// Plan 校验错误(设计文档 §十三 0.7 枚举 + 路线图 0.7 五类非法补充)。 /// /// 对齐设计文档枚举:`CycleDetected / UnknownDep / OrphanSubtask / SubtaskWithoutTools`, /// 并补充路线图要求的五类非法(Empty/DuplicateId/TooDeep)+ 实现细化(SelfDep/EmptyId)。 #[derive(Debug, Clone, PartialEq, Eq)] pub enum PlanError { /// 空 Plan(路线图五类非法之一)。 Empty, /// 子任务 id 为空字符串。 EmptyId { index: usize }, /// 重复 id(路线图五类非法之一)。 DuplicateId { id: SubTaskId }, /// 悬空依赖:deps 引用不存在的 id(设计文档枚举 UnknownDep)。 UnknownDep { from: SubTaskId, unknown: SubTaskId }, /// 自环依赖:A 依赖 A(归为悬空的特例,单独报便于定位)。 SelfDep { id: SubTaskId }, /// 环(设计文档枚举 CycleDetected)。`cycle` = 环上节点 id 序列。 CycleDetected { cycle: Vec }, /// 子任务无工具(设计文档枚举 SubtaskWithoutTools)。 SubtaskWithoutTools { id: SubTaskId }, /// 依赖链超深(路线图五类非法之一)。 TooDeep { depth: usize, max: usize }, /// 孤立子任务(设计文档枚举 OrphanSubtask 占位,当前 validate 不报,保留供 Phase 2 决策)。 #[allow(dead_code)] OrphanSubtask { id: SubTaskId }, } impl std::fmt::Display for PlanError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PlanError::Empty => write!(f, "Plan 为空"), PlanError::EmptyId { index } => write!(f, "第 {} 个子任务 id 为空", index), PlanError::DuplicateId { id } => write!(f, "重复子任务 id: {}", id), PlanError::UnknownDep { from, unknown } => { write!(f, "子任务 {} 依赖不存在的 id: {}", from, unknown) } PlanError::SelfDep { id } => write!(f, "子任务 {} 自环依赖", id), PlanError::CycleDetected { cycle } => { write!(f, "检测到环: {}", cycle.join(" -> ")) } PlanError::SubtaskWithoutTools { id } => write!(f, "子任务 {} 无 tool_hint", id), PlanError::TooDeep { depth, max } => { write!(f, "依赖链深度 {} 超上限 {}", depth, max) } PlanError::OrphanSubtask { id } => write!(f, "孤立子任务: {}", id), } } } impl std::error::Error for PlanError {} /// Kahn 分层时检测到环([`Plan::to_layers`] 返回)。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct CycleError { /// 环上(或因环无法放置的)节点 id 列表。 pub cycle: Vec, } impl std::fmt::Display for CycleError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Kahn 分层遇到环: {}", self.cycle.join(" -> ")) } } impl std::error::Error for CycleError {} // ---- DFS 环检测辅助(模块级,借用 checker 规避 self-borrow) ---------------- fn dfs_cycle<'a>( start: &'a SubTaskId, id_to_task: &HashMap<&'a SubTaskId, &'a SubTask>, color: &mut HashMap<&'a SubTaskId, u8>, stack: &mut Vec<&'a SubTaskId>, finished: &mut HashSet<&'a SubTaskId>, ) -> Option> { // 显式栈 DFS,避免递归(深度大时不爆栈) // stack 元素:(节点, 其 deps 的迭代位置)——此处简化:每节点完整遍历 deps // 改用工作栈 + 回溯标记 struct Frame<'b> { node: &'b SubTaskId, dep_idx: usize, } let mut work: Vec> = vec![Frame { node: start, dep_idx: 0 }]; color.insert(start, 1); // 灰 stack.push(start); while let Some(frame) = work.last_mut() { let node = frame.node; let task = match id_to_task.get(node) { Some(t) => *t, None => { // 不应发生(调用方已过滤悬空),兜底回溯 work.pop(); color.insert(node, 2); stack.pop(); continue; } }; let deps = &task.deps; if frame.dep_idx < deps.len() { let next_id = &deps[frame.dep_idx]; frame.dep_idx += 1; // 自环跳过(SelfDep 已单独报) if next_id == node { continue; } let next = match id_to_task.get(next_id) { Some(t) => *t, None => continue, // 悬空跳过 }; let next_color = *color.get(&next.id).unwrap_or(&0); match next_color { 1 => { // 命中灰节点 → 环。回溯 stack 找环段。 let mut cycle: Vec = Vec::new(); let mut found = false; for s in stack.iter() { if *s == &next.id { found = true; } if found { cycle.push((*s).clone()); } } cycle.push(next.id.clone()); // 闭合 return Some(cycle); } 0 => { color.insert(&next.id, 1); stack.push(&next.id); work.push(Frame { node: &next.id, dep_idx: 0, }); } _ => {} // 黑:已 finish,跳过 } } else { // 当前节点 deps 耗尽 → 完成 let done_node = frame.node; color.insert(done_node, 2); finished.insert(done_node); work.pop(); stack.pop(); } } None } // ---- 单测(对齐 intent.rs 52 测密度)--------------------------------------- #[cfg(test)] mod tests { use super::*; // 辅助:构造一个带工具的最小子任务 fn t(id: &str) -> SubTask { SubTask { id: id.to_string(), tool_hint: vec!["read_file".to_string()], deps: Vec::new(), group: None, intent: format!("task-{}", id), } } fn t_dep(id: &str, deps: &[&str]) -> SubTask { let mut s = t(id); s.deps = deps.iter().map(|s| s.to_string()).collect(); s } // 变体:deps 用 String(动态 id 场景,如循环构造的 n0/n1/...) fn t_dep_s>(id: &str, deps: I) -> SubTask { let mut s = t(id); s.deps = vec![deps.into()]; s } // ===== 0.8 Plan/SubTask 结构 ===== #[test] fn subtask_builder_chains() { let s = SubTask::new("a", "do a") .with_tools(vec!["read_file".into()]) .with_deps(vec!["b".into()]) .with_group("g1"); assert_eq!(s.id, "a"); assert_eq!(s.intent, "do a"); assert_eq!(s.tool_hint, vec!["read_file".to_string()]); assert_eq!(s.deps, vec!["b".to_string()]); assert_eq!(s.group.as_deref(), Some("g1")); } #[test] fn subtask_default_builder_minimal() { let s = SubTask::new("x", ""); assert!(s.tool_hint.is_empty()); assert!(s.deps.is_empty()); assert!(s.group.is_none()); } #[test] fn plan_new_empty() { let p = Plan::new(); assert!(p.is_empty()); assert_eq!(p.len(), 0); } #[test] fn plan_from_tasks_len() { let p = Plan::from_tasks(vec![t("a"), t("b")]); assert_eq!(p.len(), 2); assert!(!p.is_empty()); } #[test] fn plan_clone_eq() { let p = Plan::from_tasks(vec![t("a")]); let q = p.clone(); assert_eq!(p, q); } #[test] fn feature_flags_default_on() { // PLANNING_ENABLED 当前开(主 loop 走 filter_tool_defs_planned 规划),Phase 1 已落地。 // PLAN_HINT_ENABLED(plan_hint 函数 flag)在 plan_hint.rs 单一定义,此处不再断言。 assert!(PLANNING_ENABLED); } #[test] fn max_plan_depth_is_reasonable() { // 8 层覆盖典型软件任务分解 assert!(MAX_PLAN_DEPTH >= 4); assert!(MAX_PLAN_DEPTH <= 16); } // ===== 0.7 validate: 合法 Plan 通过 ===== #[test] fn validate_single_task_ok() { let p = Plan::from_tasks(vec![t("a")]); assert!(p.validate().is_ok(), "单任务无依赖应合法"); } #[test] fn validate_linear_chain_ok() { // a -> b -> c let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["a"]), t_dep("c", &["b"])]); assert!(p.validate().is_ok(), "线性链应合法"); } #[test] fn validate_diamond_ok() { // a -> b,c -> d (菱形) let p = Plan::from_tasks(vec![ t("a"), t_dep("b", &["a"]), t_dep("c", &["a"]), t_dep("d", &["b", "c"]), ]); assert!(p.validate().is_ok(), "菱形 DAG 应合法"); } #[test] fn validate_parallel_group_ok() { // 同 group 三个无依赖任务 let mut a = t("a"); a.group = Some("g1".into()); let mut b = t("b"); b.group = Some("g1".into()); let mut c = t("c"); c.group = Some("g1".into()); let p = Plan::from_tasks(vec![a, b, c]); assert!(p.validate().is_ok(), "并行组无依赖应合法"); } #[test] fn validate_require_tools_off_allows_empty_tools() { let mut s = t("a"); s.tool_hint.clear(); let p = Plan::from_tasks(vec![s]); let errs = p.validate_with(ValidateOptions { require_tools: false, max_depth: MAX_PLAN_DEPTH, }); assert!(errs.is_empty(), "关 require_tools 后无工具应放过"); } // ===== 0.7 validate: 五类非法各拒 ===== #[test] fn validate_empty_rejected() { let p = Plan::new(); let r = p.validate(); assert_eq!(r, Err(vec![PlanError::Empty])); } #[test] fn validate_duplicate_id_rejected() { let p = Plan::from_tasks(vec![t("a"), t("a")]); let errs = p.validate().unwrap_err(); assert!(errs.contains(&PlanError::DuplicateId { id: "a".into() })); } #[test] fn validate_dangling_dep_rejected() { // b 依赖不存在的 x let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["x"])]); let errs = p.validate().unwrap_err(); assert!(errs.contains(&PlanError::UnknownDep { from: "b".into(), unknown: "x".into() })); } #[test] fn validate_self_dep_rejected() { let p = Plan::from_tasks(vec![t_dep("a", &["a"])]); let errs = p.validate().unwrap_err(); assert!(errs.contains(&PlanError::SelfDep { id: "a".into() })); } #[test] fn validate_cycle_two_node_rejected() { // a <-> b let p = Plan::from_tasks(vec![t_dep("a", &["b"]), t_dep("b", &["a"])]); let errs = p.validate().unwrap_err(); assert!( errs.iter().any(|e| matches!(e, PlanError::CycleDetected { .. })), "双向环应拒为 CycleDetected, errs={:?}", errs ); } #[test] fn validate_cycle_three_node_rejected() { // a -> b -> c -> a let p = Plan::from_tasks(vec![ t_dep("a", &["c"]), t_dep("b", &["a"]), t_dep("c", &["b"]), ]); let errs = p.validate().unwrap_err(); assert!(errs .iter() .any(|e| matches!(e, PlanError::CycleDetected { .. }))); } #[test] fn validate_cycle_self_loop_is_selfdep_not_cycle() { // 自环优先报 SelfDep,不进入环检测(validate 早退条件) let p = Plan::from_tasks(vec![t_dep("a", &["a"]), t("b")]); let errs = p.validate().unwrap_err(); assert!(errs.contains(&PlanError::SelfDep { id: "a".into() })); // SelfDep 触发环检测跳过(不误报) assert!(!errs .iter() .any(|e| matches!(e, PlanError::CycleDetected { .. }))); } #[test] fn validate_subtask_without_tools_rejected() { let mut s = t("a"); s.tool_hint.clear(); let p = Plan::from_tasks(vec![s]); let errs = p.validate().unwrap_err(); assert!(errs.contains(&PlanError::SubtaskWithoutTools { id: "a".into() })); } #[test] fn validate_too_deep_rejected() { // 9 节点线性链(深度 9 > MAX_PLAN_DEPTH=8) let mut tasks: Vec = vec![t("n0")]; for i in 1..9 { tasks.push(t_dep_s(&format!("n{}", i), format!("n{}", i - 1))); } let p = Plan::from_tasks(tasks); let errs = p.validate().unwrap_err(); assert!( errs.iter().any(|e| matches!(e, PlanError::TooDeep { depth: 9, max: 8 })), "9 层应判 TooDeep, errs={:?}", errs ); } #[test] fn validate_depth_exactly_at_limit_ok() { // 8 节点线性链(深度 8 == MAX_PLAN_DEPTH,不超,应合法) let mut tasks: Vec = vec![t("n0")]; for i in 1..8 { tasks.push(t_dep_s(&format!("n{}", i), format!("n{}", i - 1))); } let p = Plan::from_tasks(tasks); assert!(p.validate().is_ok(), "8 层(==上限)应合法"); } #[test] fn validate_empty_id_rejected() { let mut s = t("a"); s.id = String::new(); let p = Plan::from_tasks(vec![s]); let errs = p.validate().unwrap_err(); assert!(errs .iter() .any(|e| matches!(e, PlanError::EmptyId { index: 0 }))); } #[test] fn validate_collects_multiple_errors() { // 同时:重复 id + 悬空 dep + 无工具 let mut no_tools = t("a"); no_tools.tool_hint.clear(); let p = Plan::from_tasks(vec![ no_tools, // 无工具 t("a"), // 重复 id(同 "a") t_dep("b", &["x"]), // 悬空 x ]); let errs = p.validate().unwrap_err(); assert!(errs.len() >= 3, "应一次收集 ≥3 类违规, got {} 条: {:?}", errs.len(), errs); assert!(errs.contains(&PlanError::DuplicateId { id: "a".into() })); assert!(errs.contains(&PlanError::SubtaskWithoutTools { id: "a".into() })); assert!(errs.contains(&PlanError::UnknownDep { from: "b".into(), unknown: "x".into() })); } #[test] fn validate_dangling_does_not_mask_cycle() { // 悬空 + 环共存:悬空报,环因早退不报(设计:悬空优先,环检测跳过) let p = Plan::from_tasks(vec![ t_dep("a", &["b"]), t_dep("b", &["a"]), t_dep("c", &["x"]), // 悬空 ]); let errs = p.validate().unwrap_err(); assert!(errs.contains(&PlanError::UnknownDep { from: "c".into(), unknown: "x".into() })); } #[test] fn plan_error_display_nonempty() { let e = PlanError::CycleDetected { cycle: vec!["a".into(), "b".into(), "a".into()], }; let s = format!("{}", e); assert!(s.contains("环")); assert!(s.contains("a -> b")); } #[test] fn cycle_error_display() { let e = CycleError { cycle: vec!["a".into()], }; assert!(format!("{}", e).contains("环")); } // ===== 0.7 Kahn to_layers ===== #[test] fn layers_empty_plan() { let p = Plan::new(); assert_eq!(p.to_layers().unwrap(), Vec::>::new()); } #[test] fn layers_single_task_one_layer() { let p = Plan::from_tasks(vec![t("a")]); let layers = p.to_layers().unwrap(); assert_eq!(layers.len(), 1); assert_eq!(layers[0].len(), 1); assert_eq!(layers[0][0].id, "a"); } #[test] fn layers_linear_chain_n_layers() { // a -> b -> c → 3 层各 1 节点 let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["a"]), t_dep("c", &["b"])]); let layers = p.to_layers().unwrap(); assert_eq!(layers.len(), 3); assert_eq!(layers[0][0].id, "a"); assert_eq!(layers[1][0].id, "b"); assert_eq!(layers[2][0].id, "c"); } #[test] fn layers_parallel_group_one_layer() { // a,b,c 无依赖 → 同一层(可并行) let p = Plan::from_tasks(vec![t("a"), t("b"), t("c")]); let layers = p.to_layers().unwrap(); assert_eq!(layers.len(), 1, "无依赖应单层: {:?}", layers); let ids: Vec<&str> = layers[0].iter().map(|t| t.id.as_str()).collect(); assert_eq!(ids, vec!["a", "b", "c"]); // 保持原顺序稳定 } #[test] fn layers_diamond_two_then_one() { // a -> b,c -> d // 层 0: [a], 层 1: [b, c](并行), 层 2: [d] let p = Plan::from_tasks(vec![ t("a"), t_dep("b", &["a"]), t_dep("c", &["a"]), t_dep("d", &["b", "c"]), ]); let layers = p.to_layers().unwrap(); assert_eq!(layers.len(), 3); assert_eq!(layer_ids(&layers, 0), vec!["a"]); assert_eq!(layer_ids(&layers, 1), vec!["b", "c"]); assert_eq!(layer_ids(&layers, 2), vec!["d"]); } #[test] fn layers_preserves_input_order_within_layer() { // 多个无依赖节点,同层应保持 Plan 内顺序(稳定) let p = Plan::from_tasks(vec![t("z"), t("y"), t("x")]); let layers = p.to_layers().unwrap(); assert_eq!(layer_ids(&layers, 0), vec!["z", "y", "x"]); } #[test] fn layers_cycle_returns_err() { // a <-> b let p = Plan::from_tasks(vec![t_dep("a", &["b"]), t_dep("b", &["a"])]); let err = p.to_layers().unwrap_err(); // 环成员 a,b 均无法放置 let cycle_set: HashSet<&str> = err.cycle.iter().map(|s| s.as_str()).collect(); assert!(cycle_set.contains("a")); assert!(cycle_set.contains("b")); } #[test] fn layers_self_dep_treated_as_no_indeg() { // 自环:A 依赖 A。Kahn 视为无该前驱(不计入度),A 仍可放置 let p = Plan::from_tasks(vec![t_dep("a", &["a"]), t_dep("b", &["a"])]); let layers = p.to_layers().unwrap(); assert_eq!(layers.len(), 2); assert_eq!(layer_ids(&layers, 0), vec!["a"]); assert_eq!(layer_ids(&layers, 1), vec!["b"]); } #[test] fn layers_dangling_dep_ignored() { // b 依赖不存在的 x —— Kahn 忽略悬空边,b 视为入度 0 入首层 let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["x"])]); let layers = p.to_layers().unwrap(); assert_eq!(layers.len(), 1, "悬空边忽略后 a,b 同层: {:?}", layers); let ids: HashSet<&str> = layers[0].iter().map(|t| t.id.as_str()).collect(); assert!(ids.contains("a")); assert!(ids.contains("b")); } #[test] fn layers_deep_chain_count_matches_validate_depth() { // 5 节点链 → 5 层 = longest_chain_depth let mut tasks: Vec = vec![t("n0")]; for i in 1..5 { tasks.push(t_dep_s(&format!("n{}", i), format!("n{}", i - 1))); } let p = Plan::from_tasks(tasks); let layers = p.to_layers().unwrap(); assert_eq!(layers.len(), 5); } #[test] fn layers_complex_dag() { // 复杂 DAG: // a b (层0) // \ / \ // c d (层1: c 依赖 a,b; d 依赖 b) // | | // e f (层2: e 依赖 c; f 依赖 d) // \ / // g (层3: g 依赖 e,f) let p = Plan::from_tasks(vec![ t("a"), t("b"), t_dep("c", &["a", "b"]), t_dep("d", &["b"]), t_dep("e", &["c"]), t_dep("f", &["d"]), t_dep("g", &["e", "f"]), ]); let layers = p.to_layers().unwrap(); assert_eq!(layers.len(), 4); assert_eq!(layer_ids(&layers, 0), vec!["a", "b"]); assert_eq!(layer_ids(&layers, 1), vec!["c", "d"]); assert_eq!(layer_ids(&layers, 2), vec!["e", "f"]); assert_eq!(layer_ids(&layers, 3), vec!["g"]); } #[test] fn layers_returns_clones_not_references() { // 返回 Vec(owned),修改不影响原 Plan let p = Plan::from_tasks(vec![t("a")]); let mut layers = p.to_layers().unwrap(); layers[0][0].intent = "modified".into(); assert_eq!(p.tasks[0].intent, "task-a", "原 Plan 不应被改"); } // 辅助 fn layer_ids(layers: &[Vec], i: usize) -> Vec<&str> { layers[i].iter().map(|t| t.id.as_str()).collect() } }