新增: 知识图谱Phase1业务层(父②) + update_task parent_id 校验(P1)
父② 业务层(②.3+②.4+②.5): - IPC: create_task 扩展 queue/parent_id/content_json + create/remove/list_task_links + move_task_queue(一致性约束联动 status) + get_task_tree - 父聚合(②.4): advance_task 后重算父 status(count_children_by_status,set_status_for_aggregation 专用路径绕 D-260616-04 status 收口,父任务=容器模型唯一非状态机写入) - AI 工具(②.5): register_task_graph_tools 分组 6 工具 + create_task 扩展,基线测试 32→38 - state.task_links: TaskLinkRepo + lib.rs 注册 5 新 IPC P1 修复(verify agent 发现,主控修): - update_task 漏 parent_id 校验(可绕 1 级嵌套创建孙任务/悬空 parent_id/自环) - 补 parent_id 特判(对标 create_task: 父存在 + 父自身无 parent_id + 自环拒) df-storage 99 lib + df-ai 331 + 基线测试 38 全过。
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
//! 任务相关命令
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
use df_types::types::{new_id, TaskStatus};
|
||||
use df_storage::crud::TaskQuery;
|
||||
use df_storage::models::TaskRecord;
|
||||
use df_storage::models::{TaskLinkRecord, TaskRecord};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -25,12 +25,94 @@ pub struct CreateTaskInput {
|
||||
/// 关联灵感 ID(F-260619-01,1对1 单向,可空=不关联)。
|
||||
/// 空字符串视为不关联(与 AI 工具层一致)。
|
||||
pub idea_id: Option<String>,
|
||||
/// 管理维度池(知识图谱 Phase 1 V29,对标设计 §2.1)。默认 "todo"(待办池)。
|
||||
/// 合法值:backlog / todo / decision / active / done。非法值在 IPC 层兜底校验。
|
||||
/// 空字符串视为默认 todo(向后兼容,与 idea_id 一致处理)。
|
||||
#[serde(default = "default_queue")]
|
||||
pub queue: String,
|
||||
/// 父任务 ID(知识图谱 Phase 1 V29)。默认 None = 叶子任务。
|
||||
/// 非空 = 子任务(限制 1 级嵌套,无孙任务:父任务自身不能有 parent_id,由 IPC 层校验)。
|
||||
/// 空字符串视为 None(向后兼容)。
|
||||
#[serde(default)]
|
||||
pub parent_id: Option<String>,
|
||||
/// 结构化需求规格 JSON 字符串(知识图谱 Phase 1 V29)。
|
||||
/// 结构 { background, acceptance_criteria[], scope[], technical_design, custom_fields }。
|
||||
/// None = 无结构化规格(纯文本 description)。
|
||||
#[serde(default)]
|
||||
pub content_json: Option<String>,
|
||||
}
|
||||
|
||||
fn default_priority() -> i32 {
|
||||
2 // medium — 新任务默认中优先级(非 high),符合常识
|
||||
}
|
||||
|
||||
/// queue 默认值(serde default,对标 DB DEFAULT 'todo')
|
||||
fn default_queue() -> String {
|
||||
"todo".to_string()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 知识图谱 Phase 1:queue 白名单 + queue/status 一致性约束(IPC 层校验,对标设计 §2.1)
|
||||
// ============================================================
|
||||
|
||||
/// queue 合法值白名单(对标设计 §2.1 queue 字段语义)。
|
||||
/// 不走 TaskStatus 枚举(queue 是独立的管理维度,与 status 执行维度正交)。
|
||||
const TASK_QUEUE_VALUES: &[&str] = &["backlog", "todo", "decision", "active", "done"];
|
||||
|
||||
/// queue 执行中池(active 时 status 必须属于执行中三态之一,对标设计 §2.1 一致性约束)
|
||||
const ACTIVE_OK_STATUSES: &[&str] = &["in_progress", "in_review", "testing"];
|
||||
|
||||
/// 校验 queue 值在白名单内,否则返回 Err(防拼写漂移/非法值进库)。
|
||||
fn validate_queue(queue: &str) -> Result<(), String> {
|
||||
if TASK_QUEUE_VALUES.contains(&queue) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"非法 queue 值 {:?},合法值: {:?}",
|
||||
queue, TASK_QUEUE_VALUES
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// queue/status 一致性约束校验(对标设计 §2.1,IPC 层校验不进状态机)。
|
||||
///
|
||||
/// 规则(设计 §2.1「queue 与 status 的关系」一致性约束):
|
||||
/// - queue=done 时 status 必须=done
|
||||
/// - queue=backlog 时 status 必须=todo
|
||||
/// - queue=active 时 status ∈ {in_progress, in_review, testing}
|
||||
/// - status=blocked 时 queue 可为 decision 或 active(本规则约束 queue 赋值场景,不在此单独拦)
|
||||
/// - queue=todo 时 status=todo(默认);queue=decision 时无 status 强约束(待决策池可任意 status)
|
||||
///
|
||||
/// 注:create_task 仅校验 queue(新建任务 status 恒 todo),完整约束在 move_task_queue 落实。
|
||||
fn assert_queue_status_consistent(queue: &str, status: &str) -> Result<(), String> {
|
||||
match queue {
|
||||
"done" => {
|
||||
if status != "done" {
|
||||
return Err(format!(
|
||||
"一致性约束违反:queue=done 要求 status=done,当前 status={status:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
"backlog" => {
|
||||
if status != "todo" {
|
||||
return Err(format!(
|
||||
"一致性约束违反:queue=backlog 要求 status=todo,当前 status={status:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
"active" => {
|
||||
if !ACTIVE_OK_STATUSES.contains(&status) {
|
||||
return Err(format!(
|
||||
"一致性约束违反:queue=active 要求 status ∈ {:?},当前 status={status:?}",
|
||||
ACTIVE_OK_STATUSES
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {} // todo / decision 无 status 强约束
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 列出未删除任务(deleted_at IS NULL)。
|
||||
///
|
||||
/// **向后兼容铁律**:`project_id` 与 `query` 两参都可选,旧调用方不传(或只传 project_id)
|
||||
@@ -92,11 +174,66 @@ pub async fn get_task_by_id(
|
||||
}
|
||||
|
||||
/// 创建任务,返回完整记录
|
||||
///
|
||||
/// 知识图谱 Phase 1 V29(对标设计 §2.1):扩展 queue/parent_id/content_json 可选参数(向后兼容,
|
||||
/// 旧调用方不传等价改造前行为)。三个新参数的 IPC 层校验:
|
||||
/// - `queue`:白名单校验(validate_queue)+ queue/status 一致性(create_task 时 status 恒 todo,
|
||||
/// 仅 backlog/todo/decision 合法;active/done 需经 move_task_queue 或 advance_task 流转)。
|
||||
/// - `parent_id`:1 级嵌套铁律(对标设计 §2.1 D2)。父任务自身不能有 parent_id(拒绝创建孙任务);
|
||||
/// 父任务不存在则拒(防悬空 parent_id)。空字符串视为 None(向后兼容)。
|
||||
/// - `content_json`:仅做轻量 JSON 合法性校验(非空时必须是合法 JSON),结构细节由 AI 消费层负责。
|
||||
#[tauri::command]
|
||||
pub async fn create_task(
|
||||
state: State<'_, AppState>,
|
||||
input: CreateTaskInput,
|
||||
) -> Result<TaskRecord, String> {
|
||||
// ── queue 校验(白名单 + 空串默认 todo)──
|
||||
// 空字符串视为默认 todo(向后兼容,与 idea_id 空串处理一致)
|
||||
let queue = if input.queue.trim().is_empty() {
|
||||
"todo".to_string()
|
||||
} else {
|
||||
let q = input.queue.trim();
|
||||
validate_queue(q)?;
|
||||
q.to_string()
|
||||
};
|
||||
// queue/status 一致性:create_task 时 status 恒 todo,仅 backlog/todo/decision 合法。
|
||||
// active/done 必须经 move_task_queue 流转,新建直接落 active/done 违反一致性约束。
|
||||
assert_queue_status_consistent(&queue, "todo")?;
|
||||
|
||||
// ── parent_id 校验(1 级嵌套铁律,对标设计 §2.1 D2)──
|
||||
// 空字符串/纯空白视为 None(向后兼容)
|
||||
let parent_id = input
|
||||
.parent_id
|
||||
.as_ref()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
if let Some(pid) = &parent_id {
|
||||
let parent = state
|
||||
.tasks
|
||||
.get_by_id(pid)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("父任务 {pid} 不存在,无法创建子任务"))?;
|
||||
// 1 级嵌套铁律:父任务自身有 parent_id → 它是子任务 → 拒绝在其下创建孙任务。
|
||||
if parent.parent_id.is_some() {
|
||||
return Err(format!(
|
||||
"违反 1 级嵌套铁律:父任务 {pid} 自身是子任务(parent_id={:?}),不允许在其下创建孙任务",
|
||||
parent.parent_id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ── content_json 轻量校验(非空时须是合法 JSON)──
|
||||
let content_json = match &input.content_json {
|
||||
Some(c) if !c.trim().is_empty() => {
|
||||
// 校验合法 JSON(防脏数据/截断串进库);结构细节由 AI 消费层负责
|
||||
serde_json::from_str::<serde_json::Value>(c)
|
||||
.map_err(|e| format!("content_json 不是合法 JSON: {e}"))?;
|
||||
Some(c.clone())
|
||||
}
|
||||
_ => None, // 空串/None 视为无结构化规格
|
||||
};
|
||||
|
||||
let now = now_millis();
|
||||
let record = TaskRecord {
|
||||
id: new_id(),
|
||||
@@ -113,12 +250,10 @@ pub async fn create_task(
|
||||
output_json: None,
|
||||
// F-260619-01:空字符串视为不关联(与 AI 工具层一致)
|
||||
idea_id: input.idea_id.filter(|s| !s.is_empty()),
|
||||
// 知识图谱 Phase 1 V29 三列:本批仅数据层迁移,create_task 参数扩展(queue/parent_id/
|
||||
// content_json 可选入参)为 Phase 1 后续 IPC 任务。此处默认值:queue='todo'(待办池)、
|
||||
// parent_id=None(叶子任务)、content_json=None(无结构化规格),与 DB 列 DEFAULT 一致。
|
||||
queue: "todo".to_string(),
|
||||
parent_id: None,
|
||||
content_json: None,
|
||||
// 知识图谱 Phase 1 V29 三列:经上方校验的 queue / parent_id / content_json
|
||||
queue,
|
||||
parent_id,
|
||||
content_json,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
};
|
||||
@@ -174,6 +309,30 @@ pub async fn update_task(
|
||||
return Err(format!("非法 project_id 值 {:?},目标项目不存在", value));
|
||||
}
|
||||
}
|
||||
// parent_id 1 级嵌套铁律(对标 create_task L203-221,verify agent 发现的 P1:update_task 绕过)。
|
||||
// parent_id 在 tasks 白名单(设计 §五 move/update 路径),但 1 级铁律须 IPC 层校验,否则
|
||||
// update_task(id,"parent_id",X) 可创建孙任务(X 自身有 parent_id)或悬空(X 不存在)或自环。
|
||||
if field == "parent_id" {
|
||||
let new_pid = value.trim();
|
||||
if !new_pid.is_empty() {
|
||||
if new_pid == id {
|
||||
return Err("非法 parent_id:不能将任务设为自身的父任务(自环)".to_string());
|
||||
}
|
||||
let parent = state
|
||||
.tasks
|
||||
.get_by_id(new_pid)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("非法 parent_id 值 {:?},父任务不存在", value))?;
|
||||
if parent.parent_id.is_some() {
|
||||
return Err(format!(
|
||||
"违反 1 级嵌套铁律:父任务 {new_pid} 自身是子任务(parent_id={:?}),不允许挂孙任务",
|
||||
parent.parent_id
|
||||
));
|
||||
}
|
||||
}
|
||||
// 空字符串 = None(解除父),合法放行
|
||||
}
|
||||
state
|
||||
.tasks
|
||||
.update_field(&id, &field, &value)
|
||||
@@ -201,6 +360,11 @@ pub async fn restore_task(state: State<'_, AppState>, id: String) -> Result<bool
|
||||
/// 流程:读当前态 → can_transition 校验 → 下沉 SQL `WHERE id AND status=expected`
|
||||
/// 防 TOCTOU → 退回转换一并 review_rounds+=1。失败均返回 Err(状态机/TOCTOU/任务不存在)。
|
||||
///
|
||||
/// **知识图谱 Phase 1 V29 父聚合(对标设计 §2.1)**:推进完成后,若推进的任务有 parent_id,
|
||||
/// 触发父任务 status 重算(recompute_parent_status)。父任务=容器模型,status 不走状态机,
|
||||
/// 由子任务聚合计算(聚合规则见 recompute_parent_status)。聚合失败不阻断推进(best-effort,
|
||||
/// 对标设计 §十一「事件流写入失败不阻断主操作」同类宽容语义)。
|
||||
///
|
||||
/// 返回:推进成功后的最新 TaskRecord(含新 status / 累加后的 review_rounds)。
|
||||
#[tauri::command]
|
||||
pub async fn advance_task(
|
||||
@@ -208,7 +372,314 @@ pub async fn advance_task(
|
||||
id: String,
|
||||
target_status: String,
|
||||
) -> Result<TaskRecord, String> {
|
||||
df_nodes::task_advance_node::advance_task_atomic(&state.tasks, &id, &target_status)
|
||||
let updated = df_nodes::task_advance_node::advance_task_atomic(
|
||||
&state.tasks,
|
||||
&id,
|
||||
&target_status,
|
||||
)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
|
||||
// 父聚合(知识图谱 Phase 1 V29):推进的子任务有 parent_id → 重算父 status。
|
||||
// best-effort:聚合失败不阻断推进(子任务已成功推进是主结果,父 status 漂移可后续修正),
|
||||
// 仅 warn 日志记录。父任务 status 不走状态机,经专用方法 set_status_for_aggregation
|
||||
// 直接写(绕过 D-260616-04 status 收口:父任务=容器模型,聚合规则是唯一非状态机写入路径)。
|
||||
if let Some(pid) = &updated.parent_id {
|
||||
if let Err(e) = recompute_parent_status(&state, pid).await {
|
||||
tracing::warn!(
|
||||
task_id = %id,
|
||||
parent_id = %pid,
|
||||
error = %e,
|
||||
"[父聚合] 重算父任务 status 失败(不阻断子任务推进)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 知识图谱 Phase 1:父任务聚合(对标设计 §2.1 父聚合规则)
|
||||
// ============================================================
|
||||
|
||||
/// 父任务 status 重算(对标设计 §2.1 聚合规则,D3 父任务=容器模型)。
|
||||
///
|
||||
/// 聚合规则(设计 §2.1「父任务推导 status」表,优先级从高到低):
|
||||
/// 1. 任一子 blocked → 父 blocked(阻塞优先,避免掩盖卡点)
|
||||
/// 2. 任一子 in_progress → 父 in_progress(执行中)
|
||||
/// 3. 全子 done/cancelled → 父 done(全部完成/取消)
|
||||
/// 4. 全子 todo → 父 todo(尚未开始)
|
||||
/// 5. 其他混合态(如 todo+done) → 父 in_progress(进行中,有进展未全完)
|
||||
///
|
||||
/// 触发时机:advance_task 子任务推进成功后,若子任务有 parent_id 则调本函数。
|
||||
/// 数据源:count_children_by_status(一次 GROUP BY 查询,数据量小无压力)。
|
||||
/// 写入:set_status_for_aggregation(父任务 status 唯一非状态机写入路径)。
|
||||
///
|
||||
/// 返回:重算后的父任务最新 status(若与当前相同则不写,返当前值)。
|
||||
async fn recompute_parent_status(state: &State<'_, AppState>, parent_id: &str) -> Result<String, String> {
|
||||
let counts = state
|
||||
.tasks
|
||||
.count_children_by_status(parent_id)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
// 无子任务(parent_id 悬空,理论上不该发生):不重算,返当前 status
|
||||
if counts.is_empty() {
|
||||
return state
|
||||
.tasks
|
||||
.get_by_id(parent_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map(|t| t.status)
|
||||
.ok_or_else(|| format!("父任务 {parent_id} 不存在"));
|
||||
}
|
||||
|
||||
// 转 HashMap<status, count> 便于按规则判定
|
||||
let map: std::collections::HashMap<String, i64> = counts.into_iter().collect();
|
||||
let total: i64 = map.values().sum();
|
||||
let blocked = map.get("blocked").copied().unwrap_or(0);
|
||||
let in_progress = map.get("in_progress").copied().unwrap_or(0);
|
||||
let todo = map.get("todo").copied().unwrap_or(0);
|
||||
let done = map.get("done").copied().unwrap_or(0);
|
||||
let cancelled = map.get("cancelled").copied().unwrap_or(0);
|
||||
|
||||
// 聚合规则判定(优先级从高到低,首个命中即定)
|
||||
let new_status = if blocked > 0 {
|
||||
"blocked".to_string()
|
||||
} else if in_progress > 0 {
|
||||
"in_progress".to_string()
|
||||
} else if (done + cancelled) == total {
|
||||
// 全 done/cancelled → done(终端态聚合为 done)
|
||||
"done".to_string()
|
||||
} else if todo == total {
|
||||
// 全 todo → todo(尚未开始)
|
||||
"todo".to_string()
|
||||
} else {
|
||||
// 其他混合态(如 todo+done, in_review+done 等)→ in_progress(进行中)
|
||||
"in_progress".to_string()
|
||||
};
|
||||
|
||||
// 读当前父 status,相同则不写(避免无谓 updated_at 抖动)
|
||||
let current = state
|
||||
.tasks
|
||||
.get_by_id(parent_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("父任务 {parent_id} 不存在"))?;
|
||||
if current.status == new_status {
|
||||
return Ok(new_status);
|
||||
}
|
||||
state
|
||||
.tasks
|
||||
.set_status_for_aggregation(parent_id, &new_status)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
Ok(new_status)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 知识图谱 Phase 1:task_link CRUD IPC(对标设计 §2.2 + §五 AI 工具表)
|
||||
// ============================================================
|
||||
|
||||
/// 创建任务横向关联(对标设计 §2.2,AI 拓扑排序编排调度的基础)。
|
||||
///
|
||||
/// 调 TaskLinkRepo::create_link(应用层校验 link_type 白名单 + depends_on 链 BFS 循环依赖检测)。
|
||||
/// 循环依赖 / 自环 / 非法 link_type 在 repo 层拒绝(对标 D8)。
|
||||
///
|
||||
/// 参数:
|
||||
/// - `task_id`:source 任务 ID(关联发起方)
|
||||
/// - `target_id`:target 任务 ID(关联指向方)
|
||||
/// - `link_type`:depends_on / blocks / relates_to(白名单校验)
|
||||
/// - `remark`:可选备注
|
||||
///
|
||||
/// 返回:插入的 link id。
|
||||
#[tauri::command]
|
||||
pub async fn create_task_link(
|
||||
state: State<'_, AppState>,
|
||||
task_id: String,
|
||||
target_id: String,
|
||||
link_type: String,
|
||||
remark: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
let id = new_id();
|
||||
state
|
||||
.task_links
|
||||
.create_link(&id, &task_id, &target_id, &link_type, remark.as_deref())
|
||||
.await
|
||||
.map_err(err_str)
|
||||
}
|
||||
|
||||
/// 删除任务关联(按 link id,对标设计 §2.2)。返回是否命中。
|
||||
#[tauri::command]
|
||||
pub async fn remove_task_link(state: State<'_, AppState>, id: String) -> Result<bool, String> {
|
||||
state.task_links.delete(&id).await.map_err(err_str)
|
||||
}
|
||||
|
||||
/// 任务关联查询入参方向(对标设计 §2.2 + §五 list_task_links)。
|
||||
///
|
||||
/// - `Outgoing`:task_id 作为 source 查其声明的全部关联(我依赖谁/我阻塞谁)
|
||||
/// - `Incoming`:task_id 作为 target 查谁指向它(谁依赖我/谁被我阻塞,反向查询)
|
||||
/// - `Both`:双向合并(去重),全量关联视图
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LinkDirection {
|
||||
Outgoing,
|
||||
Incoming,
|
||||
Both,
|
||||
}
|
||||
|
||||
impl Default for LinkDirection {
|
||||
fn default() -> Self {
|
||||
LinkDirection::Both
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询任务关联(AI 编排调度用,对标设计 §2.2 + §五 list_task_links)。
|
||||
///
|
||||
/// 参数:
|
||||
/// - `task_id`:目标任务 ID
|
||||
/// - `direction`:查询方向(Outgoing=作为 source / Incoming=作为 target / Both=双向合并),
|
||||
/// 默认 Both(全量关联视图)。空字符串/未传走默认 Both(向后兼容)。
|
||||
///
|
||||
/// 返回:关联记录列表(TaskLinkRecord,按 created_at 升序)。
|
||||
#[tauri::command]
|
||||
pub async fn list_task_links(
|
||||
state: State<'_, AppState>,
|
||||
task_id: String,
|
||||
direction: Option<LinkDirection>,
|
||||
) -> Result<Vec<TaskLinkRecord>, String> {
|
||||
let dir = direction.unwrap_or_default();
|
||||
match dir {
|
||||
LinkDirection::Outgoing => state.task_links.get_by_source(&task_id).await.map_err(err_str),
|
||||
LinkDirection::Incoming => state.task_links.get_by_target(&task_id).await.map_err(err_str),
|
||||
// 双向合并:取 Outgoing + Incoming 后按 id 去重(同一 link 在双向查询中可能出现两次)
|
||||
LinkDirection::Both => {
|
||||
let mut outgoing = state.task_links.get_by_source(&task_id).await.map_err(err_str)?;
|
||||
let incoming = state.task_links.get_by_target(&task_id).await.map_err(err_str)?;
|
||||
// 按 id 去重(Outgoing 优先保留,incoming 中 id 未出现的才追加)
|
||||
let mut seen: std::collections::HashSet<String> =
|
||||
outgoing.iter().map(|l| l.id.clone()).collect();
|
||||
for l in incoming {
|
||||
if seen.insert(l.id.clone()) {
|
||||
outgoing.push(l);
|
||||
}
|
||||
}
|
||||
// 按 created_at 升序(合并后重排,与单向查询排序一致)
|
||||
outgoing.sort_by(|a, b| a.created_at.cmp(&b.created_at));
|
||||
Ok(outgoing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 知识图谱 Phase 1:move_task_queue(跨池移动 + 一致性约束,对标设计 §2.1 + §五)
|
||||
// ============================================================
|
||||
|
||||
/// 跨池移动任务(对标设计 §2.1 queue 字段语义 + §五 move_task_queue)。
|
||||
///
|
||||
/// queue 是管理维度池(backlog/todo/decision/active/done),与 status(执行维度)正交。
|
||||
/// move_task_queue 改 queue,同时按一致性约束联动调整 status(对标设计 §2.1):
|
||||
/// - queue=done → status 强制=done(池完成即任务完成)
|
||||
/// - queue=backlog → status 强制=todo(需求池任务尚未开始)
|
||||
/// - queue=active → status 若不在 {in_progress,in_review,testing} 则强制=in_progress
|
||||
/// - queue=todo → status 若非 todo 则强制=todo(待办池任务尚未开始)
|
||||
/// - queue=decision → status 不变(待决策池可保留任意执行态,暂停推进但执行态保留)
|
||||
///
|
||||
/// 父任务(容器模型)也可 move_task_queue(其 status 由聚合规则管,本命令仅联动改 status
|
||||
/// 以满足一致性约束,聚合规则在子任务推进时仍会重算)。
|
||||
///
|
||||
/// queue/status 均走白名单校验(status 写入用专用 set_status_for_aggregation 绕过 status 收口,
|
||||
/// 因 move_task_queue 是合法的非状态机 status 联动路径,非 advance_task 状态机路径)。
|
||||
#[tauri::command]
|
||||
pub async fn move_task_queue(
|
||||
state: State<'_, AppState>,
|
||||
id: String,
|
||||
new_queue: String,
|
||||
) -> Result<TaskRecord, String> {
|
||||
let new_queue = new_queue.trim().to_string();
|
||||
validate_queue(&new_queue)?;
|
||||
|
||||
// 读当前任务(取当前 status 做一致性联动决策)
|
||||
let current = state
|
||||
.tasks
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("任务 {id} 不存在"))?;
|
||||
|
||||
// 一致性约束联动:根据 new_queue 决定 status 是否需调整
|
||||
let new_status = match new_queue.as_str() {
|
||||
"done" => "done".to_string(),
|
||||
"backlog" => "todo".to_string(),
|
||||
"active" => {
|
||||
if ACTIVE_OK_STATUSES.contains(¤t.status.as_str()) {
|
||||
current.status.clone() // 已在执行中三态,保留
|
||||
} else {
|
||||
"in_progress".to_string() // 否则强制进 in_progress(执行中池默认执行态)
|
||||
}
|
||||
}
|
||||
"todo" => "todo".to_string(), // 待办池任务 status 强制=todo(从 active 退回 todo 池即重置执行态)
|
||||
"decision" => current.status.clone(), // 待决策池保留当前 status(暂停推进不重置执行态)
|
||||
_ => unreachable!("validate_queue 已收口"),
|
||||
};
|
||||
|
||||
// 写 queue:走通用 update_field(queue 已在 tasks 白名单登记,知识图谱 Phase 1 V29 新增)。
|
||||
// 与 current.queue 不同才写(避免无谓 updated_at 抖动)。
|
||||
if current.queue != new_queue {
|
||||
state
|
||||
.tasks
|
||||
.update_field(&id, "queue", &new_queue)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
}
|
||||
// 写 status(专用 set_status_for_aggregation 绕过 status 收口,move_task_queue 是合法非状态机路径)
|
||||
if current.status != new_status {
|
||||
state
|
||||
.tasks
|
||||
.set_status_for_aggregation(&id, &new_status)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
}
|
||||
|
||||
// 回读最新记录返回
|
||||
state
|
||||
.tasks
|
||||
.get_by_id(&id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("任务 {id} 不存在(移动后回读失败)"))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 知识图谱 Phase 1:get_task_tree(父子任务树,对标设计 §2.1 + §五)
|
||||
// ============================================================
|
||||
|
||||
/// 任务树节点(父 + 子任务列表,对标设计 §2.1 限 1 级嵌套)。
|
||||
///
|
||||
/// 设计限 1 级嵌套(无孙任务,D2),故树结构是扁平的「父 + 直接子任务列表」,无需递归。
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TaskTreeNode {
|
||||
/// 父任务(根节点,可能是叶子任务无子)
|
||||
pub parent: TaskRecord,
|
||||
/// 直接子任务列表(parent_id 指向 parent 的任务,按 created_at 升序)。叶子任务时为空。
|
||||
pub children: Vec<TaskRecord>,
|
||||
}
|
||||
|
||||
/// 获取任务父子树(对标设计 §2.1 + §五 get_task_tree)。
|
||||
///
|
||||
/// 限 1 级嵌套(设计 D2:无孙任务),故本方法取「指定任务 + 其直接子任务」,不递归。
|
||||
///
|
||||
/// 参数 `parent_id`:任务 ID(无论它是叶子还是父任务,都返回其自身 + 子任务列表)。
|
||||
/// AI 用途:查看需求分解结构、子任务进度聚合(设计 §六 ⑥⑦ AI 自检 + 父聚合)。
|
||||
#[tauri::command]
|
||||
pub async fn get_task_tree(
|
||||
state: State<'_, AppState>,
|
||||
parent_id: String,
|
||||
) -> Result<TaskTreeNode, String> {
|
||||
let parent = state
|
||||
.tasks
|
||||
.get_by_id(&parent_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("任务 {parent_id} 不存在"))?;
|
||||
let children = state.tasks.get_children(&parent_id).await.map_err(err_str)?;
|
||||
Ok(TaskTreeNode { parent, children })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user