Skip to content

Commit 368b2d8

Browse files
committed
refactor(db_goals): 拆 AI 规划域至 db_goals_plan.rs
1 parent e41023a commit 368b2d8

3 files changed

Lines changed: 238 additions & 213 deletions

File tree

‎app/src-tauri/src/db_goals.rs‎

Lines changed: 4 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use rusqlite::{params, Connection};
1313
use crate::db::{unix_seconds, Db};
1414
use crate::error::Result;
1515
use crate::goal_schema::{
16-
Goal, NewGoal, NewMilestone, CRITERIA_GROUP_SETTLED, MILESTONE_DONE,
16+
Goal, NewGoal, CRITERIA_GROUP_SETTLED, MILESTONE_DONE,
1717
MILESTONE_IN_PROGRESS, MILESTONE_PENDING,
1818
};
1919
use self::milestone::add_milestone_row;
@@ -22,6 +22,9 @@ use self::milestone::add_milestone_row;
2222
/// 里程碑域:里程碑 CRUD + 三写路径共用的插入行 helper。
2323
#[path = "db_goals_milestone.rs"]
2424
mod milestone;
25+
/// AI 规划域:规划落库事务 / 体系链接 / 概念活动 / 规划上下文。
26+
#[path = "db_goals_plan.rs"]
27+
mod plan;
2528

2629
/// 三表 DDL + 索引(幂等:CREATE TABLE IF NOT EXISTS;旧库升级自动补表)。
2730
pub(crate) fn init(conn: &Connection) -> Result<()> {
@@ -358,216 +361,6 @@ impl Db {
358361
})
359362
})
360363
}
361-
362-
// ─────────────────────── v0.18.2 AI 规划(REQ-251/253) ───────────────────────
363-
364-
/// 规划确认流落库核心(v0.18.2 REQ-251——事务:里程碑/绑定/周契约);
365-
/// 体系骨架创建在命令层逐条执行(create/link 独立事务——部分失败不污染
366-
/// 目标层,设计 §四)。周契约落到目标主组(第一个绑定组——弹性承诺
367-
/// 纪律不变:仍是用户可改的自设目标)。
368-
/// @ai-context: replace_milestones=true(创建向导:AI 草案替换规则草案;
369-
/// 详情页重规划=false 增量追加——语义差异可预期可追溯)。
370-
pub fn apply_plan_core(
371-
&self,
372-
goal_id: i64,
373-
milestones: &[NewMilestone],
374-
group_ids: &[i64],
375-
contract: Option<(i64, i64, i64, i64)>, // (group_id, target_days, target_cards, week_start)
376-
replace_milestones: bool,
377-
) -> Result<()> {
378-
let now = unix_seconds();
379-
self.with_conn(|conn| {
380-
let tx = conn.unchecked_transaction()?;
381-
if replace_milestones {
382-
tx.execute("DELETE FROM goal_milestones WHERE goal_id = ?1", params![goal_id])?;
383-
}
384-
for (idx, m) in milestones.iter().enumerate() {
385-
add_milestone_row(&tx, goal_id, m, idx as i64, now)?;
386-
}
387-
for gid in group_ids {
388-
tx.execute(
389-
"INSERT OR IGNORE INTO goal_groups (goal_id, group_id, added_at) VALUES (?1, ?2, ?3)",
390-
params![goal_id, gid, now],
391-
)?;
392-
}
393-
if let Some((gid, days, cards, week_start)) = contract {
394-
tx.execute(
395-
"INSERT INTO contracts (group_id, week_start, target_days, target_cards, created_at)
396-
VALUES (?1, ?2, ?3, ?4, ?5)
397-
ON CONFLICT(group_id, week_start) DO UPDATE SET
398-
target_days = excluded.target_days,
399-
target_cards = excluded.target_cards,
400-
created_at = excluded.created_at",
401-
params![gid, week_start, days, cards, now],
402-
)?;
403-
}
404-
tx.commit()?;
405-
Ok(())
406-
})
407-
}
408-
409-
/// 目标↔体系引用(knowledge_links target_type='goal'——确认流落库用;
410-
/// 幂等:同 (system_id, goal) 已存在返回 false——防双击双链(与
411-
/// link_knowledge_target 幂等语义一致;knowledge_links 无 UNIQUE 约束,
412-
/// 查重先于插入)。
413-
pub fn link_goal_to_system(&self, goal_id: i64, system_id: i64) -> Result<bool> {
414-
self.with_conn(|conn| {
415-
let exists: i64 = conn.query_row(
416-
"SELECT COUNT(*) FROM knowledge_links
417-
WHERE system_id = ?1 AND target_type = 'goal' AND target_id = ?2",
418-
params![system_id, goal_id],
419-
|r| r.get(0),
420-
)?;
421-
if exists > 0 {
422-
return Ok(false);
423-
}
424-
conn.execute(
425-
"INSERT INTO knowledge_links (system_id, target_type, target_id, created_at)
426-
VALUES (?1, 'goal', ?2, ?3)",
427-
params![system_id, goal_id, unix_seconds()],
428-
)?;
429-
Ok(true)
430-
})
431-
}
432-
433-
/// 目标绑定体系列表(体系页「服务于目标 X」反查数据源)。
434-
/// 登记豁免 dead_code:确认流 apply(link 动作)+ 反查 UI 接线后续批次启用。
435-
#[allow(dead_code)]
436-
pub fn goal_systems(&self, goal_id: i64) -> Result<Vec<(i64, String)>> {
437-
self.with_conn(|conn| {
438-
let mut stmt = conn.prepare(
439-
"SELECT s.id, s.name FROM knowledge_systems s
440-
JOIN knowledge_links l ON l.system_id = s.id
441-
WHERE l.target_type = 'goal' AND l.target_id = ?1
442-
ORDER BY s.id ASC",
443-
)?;
444-
let rows = stmt.query_map(params![goal_id], |r| Ok((r.get(0)?, r.get(1)?)))?;
445-
rows.collect::<rusqlite::Result<Vec<_>>>().map_err(Into::into)
446-
})
447-
}
448-
449-
/// 目标内概念活动信号(绑定组挂接体系概念 + 应用/引用计数——M3 真实化)。
450-
///
451-
/// @ai-context: 应用时刻不依赖 SQLite json_each(历史 used_refs 非法会中断
452-
/// 查询——防注入面同款防御):application 行拉到 Rust 解析
453-
/// (应用记录数少,单价可忽略;UsedRefs serde 契约 camelCase)。
454-
pub fn goal_concept_activities(&self, goal_id: i64) -> Result<Vec<crate::concept_weakness::ConceptActivity>> {
455-
self.with_conn(|conn| {
456-
let mut stmt = conn.prepare(
457-
"SELECT c.id, c.name,
458-
(SELECT COUNT(*) FROM knowledge_links l2 WHERE l2.concept_id = c.id),
459-
(SELECT MAX(l3.created_at) FROM knowledge_links l3 WHERE l3.concept_id = c.id)
460-
FROM knowledge_concepts c
461-
WHERE c.id IN (
462-
SELECT DISTINCT l.concept_id FROM knowledge_links l
463-
WHERE l.target_type = 'note_group'
464-
AND l.target_id IN (SELECT group_id FROM goal_groups WHERE goal_id = ?1)
465-
AND l.concept_id IS NOT NULL
466-
) ORDER BY c.name ASC",
467-
)?;
468-
let mut acts: Vec<crate::concept_weakness::ConceptActivity> = stmt
469-
.query_map(params![goal_id], |r| {
470-
Ok(crate::concept_weakness::ConceptActivity {
471-
concept_id: r.get(0)?,
472-
name: r.get(1)?,
473-
ref_count: r.get::<_, i64>(2)? as usize,
474-
last_applied_at: None,
475-
last_referenced_at: r.get(3)?,
476-
})
477-
})?
478-
.collect::<rusqlite::Result<Vec<_>>>()?;
479-
// 应用记录(Rust 判定概念命中——防坏 JSON 中断)
480-
let mut app_stmt = conn.prepare(
481-
"SELECT used_refs, decided_at FROM knowledge_decisions WHERE kind = 'application'",
482-
)?;
483-
let apps = app_stmt
484-
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))?
485-
.collect::<rusqlite::Result<Vec<_>>>()?;
486-
for (used_refs, decided_at) in apps {
487-
let Ok(parsed) = serde_json::from_str::<crate::types::UsedRefs>(&used_refs) else { continue };
488-
for c in &mut acts {
489-
if parsed.concept_ids.contains(&c.concept_id) {
490-
c.last_applied_at = Some(c.last_applied_at.map_or(decided_at, |cur| cur.max(decided_at)));
491-
}
492-
}
493-
}
494-
Ok(acts)
495-
})
496-
}
497-
498-
/// 目标规划上下文(AI 输入——库即记忆最小面:目标/组/体系/概念/弱项/
499-
/// 最近笔记标题;返回 JSON 文本——prompt 用户侧)。
500-
/// @ai-context: now_secs 预留给未来信号窗口(当前取数不含时间窗——保持
501-
/// 签名单一(调用方不受影响);90 天口径在摘要/弱项纯函数侧。
502-
pub fn goal_plan_context(&self, goal_id: i64, _now_secs: i64) -> Result<String> {
503-
let group_ids = self.list_goal_group_ids(goal_id)?;
504-
self.with_conn(|conn| {
505-
// 组简报(名称/领域/笔记数/卡数)
506-
let mut group_rows = Vec::new();
507-
for gid in &group_ids {
508-
let row: Option<(String, Option<String>, i64, i64)> = conn
509-
.query_row(
510-
"SELECT g.name, g.domain_tag,
511-
(SELECT COUNT(*) FROM notes WHERE group_id = g.id),
512-
(SELECT COUNT(*) FROM flashcards WHERE group_id = g.id)
513-
FROM note_groups g WHERE g.id = ?1",
514-
params![gid],
515-
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
516-
)
517-
.ok();
518-
if let Some((name, domain, notes, cards)) = row {
519-
group_rows.push(serde_json::json!({
520-
"id": gid, "name": name, "domain": domain, "notes": notes, "cards": cards,
521-
}));
522-
}
523-
}
524-
// 体系/概念清单(绑定组→体系链接)
525-
let mut systems = Vec::new();
526-
let mut concepts = Vec::new();
527-
if !group_ids.is_empty() {
528-
let mut s_stmt = conn.prepare(
529-
"SELECT DISTINCT s.id, s.name FROM knowledge_systems s
530-
JOIN knowledge_links l ON l.system_id = s.id
531-
WHERE l.target_type = 'note_group'
532-
AND l.target_id IN (SELECT group_id FROM goal_groups WHERE goal_id = ?1)",
533-
)?;
534-
for row in s_stmt.query_map(params![goal_id], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?)))? {
535-
let (id, name) = row?;
536-
systems.push(serde_json::json!({ "id": id, "name": name }));
537-
}
538-
let mut c_stmt = conn.prepare(
539-
"SELECT DISTINCT c.id, c.name FROM knowledge_concepts c
540-
JOIN knowledge_links l ON l.concept_id = c.id
541-
WHERE l.target_type = 'note_group'
542-
AND l.target_id IN (SELECT group_id FROM goal_groups WHERE goal_id = ?1)",
543-
)?;
544-
for row in c_stmt.query_map(params![goal_id], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?)))? {
545-
let (id, name) = row?;
546-
concepts.push(serde_json::json!({ "id": id, "name": name }));
547-
}
548-
}
549-
// 最近笔记标题(检索片段最小面——L3 占位:目标组 top 5 更新时间)
550-
let mut recent_notes = Vec::new();
551-
if !group_ids.is_empty() {
552-
let mut n_stmt = conn.prepare(
553-
"SELECT title FROM notes WHERE group_id IN
554-
(SELECT group_id FROM goal_groups WHERE goal_id = ?1)
555-
ORDER BY updated_at DESC LIMIT 5",
556-
)?;
557-
for row in n_stmt.query_map(params![goal_id], |r| r.get::<_, String>(0))? {
558-
recent_notes.push(row?);
559-
}
560-
}
561-
let ctx = serde_json::json!({
562-
"goalId": goal_id,
563-
"groups": group_rows,
564-
"systems": systems,
565-
"concepts": concepts,
566-
"recentNotes": recent_notes,
567-
});
568-
Ok(ctx.to_string())
569-
})
570-
}
571364
}
572365

573366
/// goals 行 → Goal。

0 commit comments

Comments
 (0)