优化: 弱模型治理收尾(AC-2探索预算:单轮上限+目录去重+禁绕行 + AC-5失败画像:合法目标回灌+相近锚点提示)

This commit is contained in:
lxy
2026-08-08 19:38:53 +08:00
parent 02c8d8e5ea
commit fe780c0084
8 changed files with 648 additions and 12 deletions
+14 -2
View File
@@ -35,6 +35,7 @@ use crate::task_state_machine::{can_transition, is_regression, is_valid_state, A
/// 3. 状态机(三类拒绝,错误区分供前端分辨):
/// - 同态拒绝(from==to):Validation「相同状态,无需推进」(非状态机违例,是空操作)
/// - 非法转换(跳态/终态后继等):InvalidState「非法状态转换 X→Y」(含 from/to 上下文)
/// - 两类拒绝均附 legal_targets(from) 合法目标列表,供 LLM 下次选对目标态(AC-5)
/// - can_transition 闸门矩阵判否即此分支
/// 4. 原子写:advance_status_atomic CAS,to 是退回转换时 bump_rounds=true
/// 5. CAS 失败(affected==0):状态已被并发改动 → InvalidState 错误(防 TOCTOU 静默成功)
@@ -63,15 +64,26 @@ pub async fn advance_task_atomic(
// 3. 状态机校验(三类拒绝,错误类型区分供前端分辨):
// - 同态(from==to):Validation「相同状态,无需推进」(空操作,非状态机违例)
// - 非法转换(跳态/终态无后继等):InvalidState「非法状态转换 X→Y」
// - 两类错误均附加 legal_targets(from) 合法目标列表(当前态→可去态),
// 让 LLM 下次直接选对目标态,降低状态机拒绝的往返次数(AC-5 机制降失败)。
let from = current.status.as_str();
let legal_hint = |from: &str| -> String {
let legal = crate::task_state_machine::legal_targets(from);
if legal.is_empty() {
format!("{from} 是终态, 无合法后继")
} else {
format!("{from} 的合法目标: {}", legal.join("/"))
}
};
if from == target_status {
return Err(df_types::error::Error::Validation(format!(
"相同状态 {from:?},无需推进"
"相同状态 {from:?},无需推进,{}",
legal_hint(from)
)));
}
if !can_transition(from, target_status) {
return Err(df_types::error::Error::InvalidState {
current: format!("{from}→{target_status}(非法状态转换)"),
current: format!("{from}→{target_status}(非法状态转换), {}", legal_hint(from)),
expected: target_status.to_string(),
});
}
+47
View File
@@ -112,6 +112,20 @@ pub fn can_transition(from: &str, to: &str) -> bool {
table.get(&(from, to)).copied().unwrap_or(false)
}
/// 返回从 `from` 出发的所有合法目标状态列表。
///
/// 供 advance_task 错误提示复用:状态机拒绝非法转换时,把「当前状态 + 合法目标」附进
/// 错误信息,让 LLM 下次直接选对目标态,避免靠猜反复触发状态机拒绝。
/// 遍历 ALL_STATES 过滤 can_transition,与状态机矩阵单一真相源对齐(矩阵改动自动同步)。
/// 终态(done/cancelled)无合法后继 → 返回空列表,调用方据此提示「终态无后继」。
pub fn legal_targets(from: &str) -> Vec<&'static str> {
ALL_STATES
.iter()
.copied()
.filter(|to| can_transition(from, to))
.collect()
}
/// 判定一次转换是否为「退回」(review_rounds 应 +1)。
///
/// 退回语义:任务从前向推进阶段回退到更早的推进阶段,意味着上一轮产出未过闸门、
@@ -229,6 +243,39 @@ mod tests {
assert!(!can_transition(BLOCKED, DONE));
}
// ---------- legal_targets(错误提示用合法目标列表) ----------
#[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!(legal_targets(DONE).is_empty());
assert!(legal_targets(CANCELLED).is_empty());
}
#[test]
fn legal_targets_all_returned_values_are_valid_and_allowed() {
// 性质断言:任何合法目标都必须是合法状态值且 can_transition 放行(防列表与矩阵漂移)
for from in ALL_STATES {
for to in legal_targets(from) {
assert!(is_valid_state(to), "legal_targets({from}) 返回 {to:?} 不是合法状态值");
assert!(can_transition(from, to), "legal_targets({from}) 含 {to:?} 但 can_transition 拒绝");
}
}
}
#[test]
fn legal_targets_unknown_state_empty() {
// 未知态防御性 → 空列表(不 panic,调用方提示无合法后继)
assert!(legal_targets("merged").is_empty());
assert!(legal_targets("").is_empty());
}
// ---------- 未知状态 ----------
#[test]