33//! @ai-context: 本层只做参数校验、调用数据层/纯函数、错误映射(AGENTS.md §6)。
44//! 访谈校验:第 1/3 问必答(访谈模式 tier+scenario 缺一即拒);
55//! 快速模式(tier=None)判据走默认档——「访谈绝不允许变成负担」。
6- //! @ai-context: 埋点(metrics_events kind 扩展契约):goal_created /
7- //! goal_milestone_done(M1 写);self_test_passed/failed 仅登记
6+ //! @ai-context: 埋点(metrics_events kind 扩展契约):goal_created(本文件写)/
7+ //! goal_milestone_done(commands_goals_milestones.rs 写);
8+ //! self_test_passed/failed 仅登记
89//! 占位契约(M3 真实化),本版不写。
910//! @ai-context: inner 函数统一收 &Db(commands_groups/commands_settlement 先例)
1011//! ——内存库单测直连,不构造重量级 AppState。
12+ //! @ai-context: 职责分布(批 0-C3 Task 6 拆出 3 个平铺子模块,`#[path]` 由本文件自己
13+ //! 声明 ⇒ lib.rs 的 `mod commands_goals;` 零改动):读侧视图 → views;
14+ //! 里程碑与目标↔组绑定写入 → milestones;访谈输入契约与归一 → intent。
1115
12- use serde:: Deserialize ;
1316use tauri:: State ;
1417
1518use crate :: commands:: { normalize_title, AppState } ;
19+ use crate :: commands_goals:: intent:: { bounded, build_intent, parse_domain, GoalCreateInput } ;
1620use crate :: db:: { unix_seconds, Db } ;
1721use crate :: goal_interview:: { derive_criteria, horizon_end_secs} ;
1822use crate :: goal_progress:: GoalSignals ;
1923use crate :: goal_schema:: {
20- Goal , GoalIntent , NewGoal , NewMilestone , SuccessCriteria , CRITERIA_MANUAL , TIER_DEFAULT ,
24+ Goal , NewGoal , NewMilestone , SuccessCriteria , CRITERIA_MANUAL , TIER_DEFAULT ,
2125} ;
22- use crate :: video_profile_domain:: DomainKind ;
2326
2427/// 读侧视图层(5 个视图 DTO + 3 条读命令 + 3 个读 inner;0 emit)。
2528/// `pub(crate)`:注册清单在 app_commands.rs(crate 根的兄弟模块)按 `commands_goals::views::x` 解析。
@@ -31,54 +34,13 @@ pub(crate) mod views;
3134#[ path = "commands_goals_milestones.rs" ]
3235pub ( crate ) mod milestones;
3336
37+ /// 访谈输入契约与归一(2 个输入 DTO + 4 个纯助手;无命令、无 DB、无 IO)。
38+ /// `pub(crate)`:父模块与 `commands_goals_lifecycle_tests.rs` / `_plan_tests.rs` 按名导入。
39+ #[ path = "commands_goals_intent.rs" ]
40+ pub ( crate ) mod intent;
41+
3442/// 一周秒数(草案 due_at 换算:第 N 周 = created_at + N*7d)。
3543const WEEK_SECS : i64 = 7 * 86_400 ;
36- /// 访谈答案文本上限(防御超大 payload;chips/填空兜底的答案均为短文本)。
37- const INTENT_FIELD_MAX : usize = 200 ;
38-
39- /// 新建目标入参(前端访谈结果全量提交;serde camelCase 契约)。
40- #[ derive( Debug , Clone , Deserialize ) ]
41- #[ serde( rename_all = "camelCase" ) ]
42- pub struct GoalCreateInput {
43- pub name : String ,
44- #[ serde( default ) ]
45- pub domain_tag : Option < String > ,
46- /// 时限(3m/6m/none/2w;None=未填)
47- #[ serde( default ) ]
48- pub horizon : Option < String > ,
49- /// 判据档位(None=快速模式→默认档)
50- #[ serde( default ) ]
51- pub tier : Option < String > ,
52- #[ serde( default ) ]
53- pub scenario : Option < String > ,
54- #[ serde( default ) ]
55- pub level : Option < String > ,
56- #[ serde( default ) ]
57- pub driver : Option < String > ,
58- #[ serde( default ) ]
59- pub criteria_statement : Option < String > ,
60- #[ serde( default ) ]
61- pub non_scope : Option < String > ,
62- #[ serde( default ) ]
63- pub weekly_commitment : Option < String > ,
64- #[ serde( default ) ]
65- pub obstacles : Option < String > ,
66- /// 初始绑定组(访谈第 4 步预勾选)
67- #[ serde( default ) ]
68- pub group_ids : Vec < i64 > ,
69- /// 里程碑草案(宣言页预填可删改;due_weeks=0 无期限)
70- #[ serde( default ) ]
71- pub milestones : Vec < GoalMilestoneInput > ,
72- }
73-
74- /// 里程碑草案输入。
75- #[ derive( Debug , Clone , Deserialize ) ]
76- #[ serde( rename_all = "camelCase" ) ]
77- pub struct GoalMilestoneInput {
78- pub title : String ,
79- #[ serde( default ) ]
80- pub due_weeks : usize ,
81- }
8244
8345/// 新建目标(访谈确认后一步创建——status=active,无 draft 仪式)。
8446#[ tauri:: command]
@@ -295,40 +257,6 @@ pub(crate) fn update_goal_status_inner(db: &Db, id: i64, status: &str) -> Result
295257 db. set_goal_status ( id, status) . map_err ( |e| e. to_string ( ) )
296258}
297259
298- /// 领域标签校验(与 commands_groups 同口径:kebab-case 白名单;空 → None)。
299- fn parse_domain ( domain_tag : Option < & str > ) -> Result < Option < String > , String > {
300- match domain_tag {
301- Some ( t) if !t. trim ( ) . is_empty ( ) => DomainKind :: parse ( t. trim ( ) )
302- . map ( |k| Some ( k. as_str ( ) . to_string ( ) ) )
303- . ok_or_else ( || format ! ( "不支持的领域标签: {}" , t) ) ,
304- _ => Ok ( None ) ,
305- }
306- }
307-
308- /// 空白串归一为 None(访谈答案「跳过/以后想」的存储语义:不落空串)。
309- fn trimmed ( s : Option < & str > ) -> Option < String > {
310- s. map ( str:: trim) . filter ( |v| !v. is_empty ( ) ) . map ( str:: to_string)
311- }
312-
313- /// 访谈答案:空白归一 + 长度截断(防超大 payload 入库;truncate 保留前 200 字)。
314- fn bounded ( s : Option < & str > ) -> Option < String > {
315- trimmed ( s) . map ( |v| v. chars ( ) . take ( INTENT_FIELD_MAX ) . collect ( ) )
316- }
317-
318- /// 访谈答案 → GoalIntent(全部可选字段过 bounded 归一)。
319- fn build_intent ( input : & GoalCreateInput ) -> GoalIntent {
320- GoalIntent {
321- scenario : bounded ( input. scenario . as_deref ( ) ) ,
322- level : bounded ( input. level . as_deref ( ) ) ,
323- driver : bounded ( input. driver . as_deref ( ) ) ,
324- criteria_statement : bounded ( input. criteria_statement . as_deref ( ) ) ,
325- horizon : bounded ( input. horizon . as_deref ( ) ) ,
326- non_scope : bounded ( input. non_scope . as_deref ( ) ) ,
327- weekly_commitment : bounded ( input. weekly_commitment . as_deref ( ) ) ,
328- obstacles : bounded ( input. obstacles . as_deref ( ) ) ,
329- }
330- }
331-
332260/// 进度信号收集(详情/列表共用——口径单一;lifecycle 命令组复用)。
333261pub ( crate ) fn collect_signals ( db : & Db , goal_id : i64 ) -> Result < GoalSignals , String > {
334262 db. goal_progress_signals ( goal_id, unix_seconds ( ) ) . map_err ( |e| e. to_string ( ) )
0 commit comments