新增: 知识图谱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:
@@ -152,6 +152,13 @@ pub fn allowed_columns_for(table: &str) -> Option<&'static [&'static str]> {
|
||||
// idea_id(F-260619-01 任务关联灵感,1对1 单向):任务可关联/解关联一条灵感,
|
||||
// 非状态机收口字段,合法可写(idea_id 存在性由外键约束 + 上层校验兜底)。
|
||||
"idea_id",
|
||||
// 知识图谱 Phase 1 V29 三列(对标设计 §2.1):非状态机收口字段,合法可写。
|
||||
// - queue:管理维度池,走 move_task_queue IPC(设计 §五),IPC 层校验白名单 + 一致性约束。
|
||||
// - parent_id:父子纵向关联,1 级嵌套铁律由 IPC 层(create_task)校验不进白名单。
|
||||
// - content_json:结构化需求规格,走未来 update_content IPC(设计 §五)。
|
||||
// 注:status 仍不列入(D-260616-04 收口不变),父聚合/move_task_queue 联动 status
|
||||
// 走专用方法 set_status_for_aggregation,不经通用 update_field。
|
||||
"queue", "parent_id", "content_json",
|
||||
"updated_at",
|
||||
// TODO(B-260616-16): project_id 跨表存在性校验待 commands/task.rs 层补。
|
||||
// 通用 CRUD 层(db repo)只懂表/列语义,不持有跨表业务约束(查 projects 表存在性)。
|
||||
|
||||
@@ -500,6 +500,38 @@ impl TaskRepo {
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 父任务聚合专用 status 写入(知识图谱 Phase 1 V29,对标设计 §2.1 D3 父任务=容器模型)。
|
||||
///
|
||||
/// **这是父任务 status 的唯一写入路径**,绕过 D-260616-04 status 收口(通用 update_field
|
||||
/// 白名单不含 status,所有叶子任务 status 走 advance_status_atomic 状态机)。父任务 status
|
||||
/// **不走状态机**(容器模型,由子任务聚合计算),故需专用写入路径。
|
||||
///
|
||||
/// 防护:
|
||||
/// - 方法名 `set_status_for_aggregation` 显式表明语义,非通用 setter,防误用。
|
||||
/// - 调用方(commands::task::recompute_parent_status)负责聚合规则计算,本方法只落库。
|
||||
/// - 不动 review_rounds(父任务不执行工作流,无 review 退回语义)。
|
||||
///
|
||||
/// 返回是否命中(父任务不存在/已删 → false)。
|
||||
pub async fn set_status_for_aggregation(&self, id: &str, new_status: &str) -> Result<bool> {
|
||||
let conn = self.conn.clone();
|
||||
let id = id.to_owned();
|
||||
let new_status = new_status.to_owned();
|
||||
let now = now_millis_str();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(
|
||||
"UPDATE tasks SET status = ?1, updated_at = ?2 \
|
||||
WHERE id = ?3 AND deleted_at IS NULL",
|
||||
params![new_status, now, id],
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 列出回收站(deleted_at IS NOT NULL),按更新时间(≈删除时间)降序。对标 ProjectRepo::list_deleted。
|
||||
///
|
||||
/// 注:按项目列活跃任务走 list_active_by_project(SQL 下推 project_id),
|
||||
@@ -735,4 +767,43 @@ mod tests {
|
||||
let ids: Vec<_> = children.iter().map(|r| r.id.as_str()).collect();
|
||||
assert_eq!(ids, vec!["c1"], "软删子任务应被过滤");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 父聚合专用 status 写入:set_status_for_aggregation(知识图谱 Phase 1 V29)
|
||||
// 父任务=容器模型,status 不走状态机,由子任务聚合计算后经此方法落库。
|
||||
// 锁定:① 写入命中 + status 变更;② 不动 review_rounds;③ 软删任务返回 false。
|
||||
// ============================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_status_for_aggregation_writes_status() {
|
||||
let repo = setup().await;
|
||||
// 父任务初始 todo(queue=todo, parent_id=None 容器模型)
|
||||
repo.insert(trec_full("parent", "todo", None, "todo"))
|
||||
.await
|
||||
.unwrap();
|
||||
let ok = repo.set_status_for_aggregation("parent", "in_progress").await.unwrap();
|
||||
assert!(ok, "应命中写入");
|
||||
let after = repo.get_by_id("parent").await.unwrap().unwrap();
|
||||
assert_eq!(after.status, "in_progress", "status 应被聚合写入更新");
|
||||
assert_eq!(after.review_rounds, 0, "父聚合写入不动 review_rounds");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_status_for_aggregation_skips_soft_deleted() {
|
||||
// 软删任务(回收站)不进聚合写入(WHERE deleted_at IS NULL),返回 false
|
||||
let repo = setup().await;
|
||||
repo.insert(trec_full("parent", "todo", None, "todo"))
|
||||
.await
|
||||
.unwrap();
|
||||
repo.soft_delete("parent").await.unwrap();
|
||||
let ok = repo.set_status_for_aggregation("parent", "done").await.unwrap();
|
||||
assert!(!ok, "软删任务不应被聚合写入命中");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_status_for_aggregation_nonexistent_returns_false() {
|
||||
let repo = setup().await;
|
||||
let ok = repo.set_status_for_aggregation("ghost", "done").await.unwrap();
|
||||
assert!(!ok, "不存在的任务应返回 false");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user