@@ -13,7 +13,7 @@ use rusqlite::{params, Connection};
1313use crate :: db:: { unix_seconds, Db } ;
1414use crate :: error:: Result ;
1515use 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} ;
1919use self :: milestone:: add_milestone_row;
@@ -22,6 +22,9 @@ use self::milestone::add_milestone_row;
2222/// 里程碑域:里程碑 CRUD + 三写路径共用的插入行 helper。
2323#[ path = "db_goals_milestone.rs" ]
2424mod milestone;
25+ /// AI 规划域:规划落库事务 / 体系链接 / 概念活动 / 规划上下文。
26+ #[ path = "db_goals_plan.rs" ]
27+ mod plan;
2528
2629/// 三表 DDL + 索引(幂等:CREATE TABLE IF NOT EXISTS;旧库升级自动补表)。
2730pub ( 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