修复: gen_stream判重+重试分类+状态机最短路径+既有测试修复
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
//! 任务推进状态机 — 7 态合法转换定义。
|
||||
//! 任务推进状态机 — 8 态合法转换定义。
|
||||
//!
|
||||
//! 独立模块,非挂在 df-types::TaskStatus enum 上(对齐 D-260616-03「推进链业务逻辑落
|
||||
//! df-nodes」)。本模块只做「给定 from/to 是否合法」的纯函数判定,不触碰存储层
|
||||
//! (原子写 SQL 在 task_advance_node.rs 完成)。
|
||||
//!
|
||||
//! 7 态(与 df-types::TaskStatus / 前端对齐,D-260616-01):
|
||||
//! todo / in_progress / in_review / testing / done / blocked / cancelled
|
||||
//! 8 态(与 df-types::TaskStatus / 前端对齐,D-260616-01):
|
||||
//! todo / in_progress / in_review / testing / done / blocked / cancelled / deferred
|
||||
//!
|
||||
//! 闸门链主路径: todo → in_progress → in_review → testing → done
|
||||
//!
|
||||
@@ -19,7 +19,7 @@
|
||||
//! blocked, cancelled
|
||||
//! - done → 终态(原则上不可变;若需重开走 cancelled 或新任务)
|
||||
//! - blocked → in_progress(解除阻塞继续), cancelled
|
||||
//! - cancelled → 终态
|
||||
//! - cancelled → todo(取消后恢复,非终态;仅 done 为终态)
|
||||
|
||||
// ============================================================
|
||||
// 状态字符串常量 — 从 df-types::TaskStatus::as_str 派生(单一真相源)
|
||||
@@ -43,8 +43,10 @@ pub const TESTING: &str = TaskStatus::Testing.as_str();
|
||||
pub const DONE: &str = TaskStatus::Done.as_str();
|
||||
/// 已阻塞
|
||||
pub const BLOCKED: &str = TaskStatus::Blocked.as_str();
|
||||
/// 已取消(终态)
|
||||
/// 已取消(可恢复为待开始)
|
||||
pub const CANCELLED: &str = TaskStatus::Cancelled.as_str();
|
||||
/// 已暂缓
|
||||
pub const DEFERRED: &str = TaskStatus::Deferred.as_str();
|
||||
|
||||
/// 全部合法状态值(供输入校验与错误提示复用)
|
||||
pub const ALL_STATES: &[&str] = &[
|
||||
@@ -55,6 +57,7 @@ pub const ALL_STATES: &[&str] = &[
|
||||
DONE,
|
||||
BLOCKED,
|
||||
CANCELLED,
|
||||
DEFERRED,
|
||||
];
|
||||
|
||||
/// 字符串是否为合法状态值
|
||||
@@ -64,7 +67,7 @@ pub fn is_valid_state(s: &str) -> bool {
|
||||
|
||||
/// 判定从 `from` 到 `to` 的状态转换是否合法(状态机核心)。
|
||||
///
|
||||
/// 终态(done/cancelled)无任何合法后继;非法或未知状态入参一律返回 false
|
||||
/// done 为唯一终态,无任何合法后继(cancelled 可恢复为 todo,非终态);非法或未知状态入参一律返回 false
|
||||
/// (调用方 advance_task 在前置校验已拦截非法 status,此处保守拒绝防漏)。
|
||||
///
|
||||
/// 注意:仅判「是否合法」,不判「是否是退回」——退回(导致 review_rounds+1)
|
||||
@@ -101,7 +104,21 @@ pub fn can_transition(from: &str, to: &str) -> bool {
|
||||
// blocked → in_progress(解除阻塞继续), cancelled
|
||||
(BLOCKED, IN_PROGRESS),
|
||||
(BLOCKED, CANCELLED),
|
||||
// cancelled → 终态,无后继
|
||||
// cancelled → todo(取消后恢复)
|
||||
(CANCELLED, TODO),
|
||||
// deferred → todo(恢复), cancelled(取消)
|
||||
(DEFERRED, TODO),
|
||||
(DEFERRED, CANCELLED),
|
||||
// todo → deferred(延后)
|
||||
(TODO, DEFERRED),
|
||||
// in_progress → deferred
|
||||
(IN_PROGRESS, DEFERRED),
|
||||
// in_review → deferred
|
||||
(IN_REVIEW, DEFERRED),
|
||||
// testing → deferred
|
||||
(TESTING, DEFERRED),
|
||||
// blocked → deferred
|
||||
(BLOCKED, DEFERRED),
|
||||
];
|
||||
for (f, t) in allowed {
|
||||
m.insert((*f, *t), true);
|
||||
@@ -117,7 +134,7 @@ pub fn can_transition(from: &str, to: &str) -> bool {
|
||||
/// 供 advance_task 错误提示复用:状态机拒绝非法转换时,把「当前状态 + 合法目标」附进
|
||||
/// 错误信息,让 LLM 下次直接选对目标态,避免靠猜反复触发状态机拒绝。
|
||||
/// 遍历 ALL_STATES 过滤 can_transition,与状态机矩阵单一真相源对齐(矩阵改动自动同步)。
|
||||
/// 终态(done/cancelled)无合法后继 → 返回空列表,调用方据此提示「终态无后继」。
|
||||
/// done 为唯一终态无合法后继 → 返回空列表,调用方据此提示「终态无后继」。
|
||||
pub fn legal_targets(from: &str) -> Vec<&'static str> {
|
||||
ALL_STATES
|
||||
.iter()
|
||||
@@ -126,6 +143,60 @@ pub fn legal_targets(from: &str) -> Vec<&'static str> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 基于 BFS 计算从 `from` 到 `to` 的最短合法转换路径(含首尾)。
|
||||
///
|
||||
/// 当 `can_transition(from, to)` 为 false 时,调用方可将此路径附进错误提示,
|
||||
/// 让 LLM 或用户知道「一步跳不到,但可以这样走」。
|
||||
///
|
||||
/// 返回路径列表,第一项为 `from`, 最后一项为 `to`。
|
||||
/// 如果 `from == to`(同态)或不存在可达路径,返回空切片。
|
||||
/// 注意:done 是唯一终态,从非终态到 done 只有主路径一条(跳闸门仍不可达);
|
||||
/// 从 done 到任何态均不可达,返回空。
|
||||
pub fn shortest_path(from: &str, to: &str) -> Vec<&'static str> {
|
||||
use std::collections::VecDeque;
|
||||
|
||||
if from == to {
|
||||
return vec![]; // 同态,无路径
|
||||
}
|
||||
if !is_valid_state(from) || !is_valid_state(to) {
|
||||
return vec![]; // 未知态
|
||||
}
|
||||
|
||||
// BFS:队列存(当前节点,路径);visited 跳过已探节点。
|
||||
let mut visited = std::collections::HashSet::<&'static str>::new();
|
||||
let mut queue: VecDeque<Vec<&'static str>> = VecDeque::new();
|
||||
// from 已知是合法状态值,unwrap 安全
|
||||
let from_static = ALL_STATES.iter().find(|s| **s == from).copied().unwrap();
|
||||
visited.insert(from_static);
|
||||
queue.push_back(vec![from_static]);
|
||||
|
||||
while let Some(path) = queue.pop_front() {
|
||||
let current = path.last().copied().unwrap();
|
||||
for candidate in ALL_STATES {
|
||||
if !can_transition(current, candidate) {
|
||||
continue;
|
||||
}
|
||||
if *candidate == to {
|
||||
let mut full = path.clone();
|
||||
full.push(candidate);
|
||||
return full;
|
||||
}
|
||||
if visited.insert(candidate) {
|
||||
let mut next = path.clone();
|
||||
next.push(candidate);
|
||||
queue.push_back(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vec![] // 不可达
|
||||
}
|
||||
|
||||
/// 格式化为人类可读路径字符串,如 "deferred → todo → in_progress → blocked"。
|
||||
pub fn format_path(path: &[&'static str]) -> String {
|
||||
path.join(" → ")
|
||||
}
|
||||
|
||||
/// 判定一次转换是否为「退回」(review_rounds 应 +1)。
|
||||
///
|
||||
/// 退回语义:任务从前向推进阶段回退到更早的推进阶段,意味着上一轮产出未过闸门、
|
||||
@@ -217,11 +288,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn terminal_states_have_no_successors() {
|
||||
// done / cancelled 是终态,任何后继都拒绝
|
||||
for term in [DONE, CANCELLED] {
|
||||
for to in ALL_STATES {
|
||||
assert!(!can_transition(term, to), "终态 {term} 不应有后继 → {to}");
|
||||
}
|
||||
// done 是唯一终态,任何后继都拒绝(cancelled 已支持取消后恢复,非终态)
|
||||
for to in ALL_STATES {
|
||||
assert!(!can_transition(DONE, to), "终态 {DONE} 不应有后继 → {to}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,14 +317,15 @@ mod tests {
|
||||
#[test]
|
||||
fn legal_targets_matches_transition_matrix() {
|
||||
// 逐态锁定合法目标列表(顺序 = ALL_STATES 遍历序),与 can_transition 矩阵一一对应
|
||||
assert_eq!(legal_targets(TODO), vec![IN_PROGRESS, CANCELLED]);
|
||||
assert_eq!(legal_targets(IN_PROGRESS), vec![IN_REVIEW, BLOCKED, CANCELLED]);
|
||||
assert_eq!(legal_targets(IN_REVIEW), vec![IN_PROGRESS, TESTING, BLOCKED, CANCELLED]);
|
||||
assert_eq!(legal_targets(TESTING), vec![IN_REVIEW, DONE, BLOCKED, CANCELLED]);
|
||||
assert_eq!(legal_targets(BLOCKED), vec![IN_PROGRESS, CANCELLED]);
|
||||
// 终态无合法后继
|
||||
assert_eq!(legal_targets(TODO), vec![IN_PROGRESS, CANCELLED, DEFERRED]);
|
||||
assert_eq!(legal_targets(IN_PROGRESS), vec![IN_REVIEW, BLOCKED, CANCELLED, DEFERRED]);
|
||||
assert_eq!(legal_targets(IN_REVIEW), vec![IN_PROGRESS, TESTING, BLOCKED, CANCELLED, DEFERRED]);
|
||||
assert_eq!(legal_targets(TESTING), vec![IN_REVIEW, DONE, BLOCKED, CANCELLED, DEFERRED]);
|
||||
assert_eq!(legal_targets(BLOCKED), vec![IN_PROGRESS, CANCELLED, DEFERRED]);
|
||||
assert_eq!(legal_targets(CANCELLED), vec![TODO]);
|
||||
assert_eq!(legal_targets(DEFERRED), vec![TODO, CANCELLED]);
|
||||
// done 是唯一终态,无合法后继
|
||||
assert!(legal_targets(DONE).is_empty());
|
||||
assert!(legal_targets(CANCELLED).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -331,7 +401,7 @@ mod tests {
|
||||
// 任何非 None 返回值都必须是 ALL_STATES 内的合法状态常量。
|
||||
// 一旦 regression_target 误返回非状态字符串(拼写漂移 / 拼了历史态如 "merged"),
|
||||
// 该字符串过不了 is_valid_state,本测试立即失败定位。
|
||||
// 覆盖全部 7 态入参,不依赖上例已锁定的具体期望值。
|
||||
// 覆盖全部 8 态入参,不依赖上例已锁定的具体期望值。
|
||||
for target in ALL_STATES {
|
||||
match regression_target(target) {
|
||||
Some(ret) => assert!(
|
||||
@@ -346,7 +416,7 @@ mod tests {
|
||||
// ---------- is_valid_state ----------
|
||||
|
||||
#[test]
|
||||
fn is_valid_state_accepts_7_known() {
|
||||
fn is_valid_state_accepts_all_known() {
|
||||
for s in ALL_STATES {
|
||||
assert!(is_valid_state(s));
|
||||
}
|
||||
@@ -362,13 +432,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_states_has_seven_entries() {
|
||||
assert_eq!(ALL_STATES.len(), 7);
|
||||
fn all_states_has_eight_entries() {
|
||||
assert_eq!(ALL_STATES.len(), 8);
|
||||
}
|
||||
|
||||
// ---------- 双源一致性校验 ----------
|
||||
//
|
||||
// task_state_machine 维护一份独立的 7 态字符串常量集(本模块 ALL_STATES / TODO / ...),
|
||||
// task_state_machine 维护一份独立的 8 态字符串常量集(本模块 ALL_STATES / TODO / ...),
|
||||
// 与 df-types::TaskStatus::as_str / valid_values() 同语义但不复用枚举(独立模块定位,
|
||||
// 推进链判定不耦合存储枚举)。两源无编译期绑定,若任一处改拼写或增删状态值,
|
||||
// 状态机判定会与数据库 status 列存值静默脱节(按常量判合法但落库值对不上)。
|
||||
@@ -407,5 +477,73 @@ mod tests {
|
||||
assert_eq!(DONE, TaskStatus::Done.as_str());
|
||||
assert_eq!(BLOCKED, TaskStatus::Blocked.as_str());
|
||||
assert_eq!(CANCELLED, TaskStatus::Cancelled.as_str());
|
||||
assert_eq!(DEFERRED, TaskStatus::Deferred.as_str());
|
||||
}
|
||||
|
||||
// ---------- shortest_path(最短路径推导) ----------
|
||||
|
||||
#[test]
|
||||
fn shortest_path_deferred_to_blocked() {
|
||||
// deferred→todo→in_progress→blocked:最短 3 跳
|
||||
let path = shortest_path(DEFERRED, BLOCKED);
|
||||
assert_eq!(path, vec![DEFERRED, TODO, IN_PROGRESS, BLOCKED]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortest_path_direct_transition() {
|
||||
// 一步可达:todo→in_progress
|
||||
let path = shortest_path(TODO, IN_PROGRESS);
|
||||
assert_eq!(path, vec![TODO, IN_PROGRESS]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortest_path_same_state_returns_empty() {
|
||||
// 同态返回空
|
||||
assert!(shortest_path(TODO, TODO).is_empty());
|
||||
assert!(shortest_path(IN_PROGRESS, IN_PROGRESS).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortest_path_from_terminal_returns_empty() {
|
||||
// done 是终态,无任何后继
|
||||
assert!(shortest_path(DONE, TODO).is_empty());
|
||||
assert!(shortest_path(DONE, IN_PROGRESS).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortest_path_unknown_state_returns_empty() {
|
||||
assert!(shortest_path("merged", TODO).is_empty());
|
||||
assert!(shortest_path(TODO, "merged").is_empty());
|
||||
assert!(shortest_path("", "").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortest_path_multi_hop_consistency() {
|
||||
// 多跳路径:第一条边必须是 legal_targets(from) 的子集
|
||||
let path = shortest_path(DEFERRED, BLOCKED);
|
||||
assert_eq!(path.len(), 4, "deferred→blocked 最短是 4 节点,实际: {:?}", path);
|
||||
// 首条边 deferred→todo 必须合法
|
||||
assert!(can_transition(path[0], path[1]));
|
||||
// 中间每条边都合法
|
||||
for i in 0..path.len() - 1 {
|
||||
assert!(
|
||||
can_transition(path[i], path[i + 1]),
|
||||
"路径边 {i}: {}→{} 不合法",
|
||||
path[i],
|
||||
path[i + 1]
|
||||
);
|
||||
}
|
||||
// 最后一段必须是 blocked
|
||||
assert_eq!(path.last().copied().unwrap(), BLOCKED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_path_joins_with_arrow() {
|
||||
assert_eq!(
|
||||
format_path(&[DEFERRED, TODO, IN_PROGRESS, BLOCKED]),
|
||||
"deferred → todo → in_progress → blocked"
|
||||
);
|
||||
assert_eq!(format_path(&[TODO, IN_PROGRESS]), "todo → in_progress");
|
||||
assert_eq!(format_path(&[]), "");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user