Skip to content

Commit ab51556

Browse files
committed
refactor(rust): 拆 commands_goals 访谈契约(344→272)
1 parent 38557f8 commit ab51556

6 files changed

Lines changed: 117 additions & 88 deletions

File tree

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

Lines changed: 13 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,26 @@
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;
1316
use tauri::State;
1417

1518
use crate::commands::{normalize_title, AppState};
19+
use crate::commands_goals::intent::{bounded, build_intent, parse_domain, GoalCreateInput};
1620
use crate::db::{unix_seconds, Db};
1721
use crate::goal_interview::{derive_criteria, horizon_end_secs};
1822
use crate::goal_progress::GoalSignals;
1923
use 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"]
3235
pub(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)。
3543
const 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 命令组复用)。
333261
pub(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())
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//! 学习目标访谈输入契约与归一(IPC DTO + 纯助手——无命令、无 DB、无 IO)。
2+
//!
3+
//! @ai-context: 从 commands_goals.rs 拆出(≤300 行约束 / AGENTS.md §3)。
4+
//! `GoalCreateInput` 是前端访谈结果的全量提交契约(serde camelCase:
5+
//! 字段名与声明顺序即 JSON 键序,**禁止改动**——前端
6+
//! `InterviewDialog`/`GraduateDialog` 测试按参数形态断言)。
7+
//! @ai-context: 归一语义:空白串一律归一为 None(访谈答案「跳过/以后想」不落空串);
8+
//! 答案截断 200 字(`INTENT_FIELD_MAX`,防超大 payload 入库)。
9+
//! @ai-context: 本文件 4 个助手均为**纯函数**(无副作用、无 IO),
10+
//! 父模块的 create_goal_inner / update_goal_inner /
11+
//! update_goal_interview_inner 复用它们 ⇒ `pub(crate)`。
12+
13+
use serde::Deserialize;
14+
15+
use crate::goal_schema::GoalIntent;
16+
use crate::video_profile_domain::DomainKind;
17+
18+
/// 访谈答案文本上限(防御超大 payload;chips/填空兜底的答案均为短文本)。
19+
const INTENT_FIELD_MAX: usize = 200;
20+
21+
/// 新建目标入参(前端访谈结果全量提交;serde camelCase 契约)。
22+
#[derive(Debug, Clone, Deserialize)]
23+
#[serde(rename_all = "camelCase")]
24+
pub struct GoalCreateInput {
25+
pub name: String,
26+
#[serde(default)]
27+
pub domain_tag: Option<String>,
28+
/// 时限(3m/6m/none/2w;None=未填)
29+
#[serde(default)]
30+
pub horizon: Option<String>,
31+
/// 判据档位(None=快速模式→默认档)
32+
#[serde(default)]
33+
pub tier: Option<String>,
34+
#[serde(default)]
35+
pub scenario: Option<String>,
36+
#[serde(default)]
37+
pub level: Option<String>,
38+
#[serde(default)]
39+
pub driver: Option<String>,
40+
#[serde(default)]
41+
pub criteria_statement: Option<String>,
42+
#[serde(default)]
43+
pub non_scope: Option<String>,
44+
#[serde(default)]
45+
pub weekly_commitment: Option<String>,
46+
#[serde(default)]
47+
pub obstacles: Option<String>,
48+
/// 初始绑定组(访谈第 4 步预勾选)
49+
#[serde(default)]
50+
pub group_ids: Vec<i64>,
51+
/// 里程碑草案(宣言页预填可删改;due_weeks=0 无期限)
52+
#[serde(default)]
53+
pub milestones: Vec<GoalMilestoneInput>,
54+
}
55+
56+
/// 里程碑草案输入。
57+
#[derive(Debug, Clone, Deserialize)]
58+
#[serde(rename_all = "camelCase")]
59+
pub struct GoalMilestoneInput {
60+
pub title: String,
61+
#[serde(default)]
62+
pub due_weeks: usize,
63+
}
64+
65+
/// 领域标签校验(与 commands_groups 同口径:kebab-case 白名单;空 → None)。
66+
pub(crate) fn parse_domain(domain_tag: Option<&str>) -> Result<Option<String>, String> {
67+
match domain_tag {
68+
Some(t) if !t.trim().is_empty() => DomainKind::parse(t.trim())
69+
.map(|k| Some(k.as_str().to_string()))
70+
.ok_or_else(|| format!("不支持的领域标签: {}", t)),
71+
_ => Ok(None),
72+
}
73+
}
74+
75+
/// 空白串归一为 None(访谈答案「跳过/以后想」的存储语义:不落空串)。
76+
fn trimmed(s: Option<&str>) -> Option<String> {
77+
s.map(str::trim).filter(|v| !v.is_empty()).map(str::to_string)
78+
}
79+
80+
/// 访谈答案:空白归一 + 长度截断(防超大 payload 入库;truncate 保留前 200 字)。
81+
pub(crate) fn bounded(s: Option<&str>) -> Option<String> {
82+
trimmed(s).map(|v| v.chars().take(INTENT_FIELD_MAX).collect())
83+
}
84+
85+
/// 访谈答案 → GoalIntent(全部可选字段过 bounded 归一)。
86+
pub(crate) fn build_intent(input: &GoalCreateInput) -> GoalIntent {
87+
GoalIntent {
88+
scenario: bounded(input.scenario.as_deref()),
89+
level: bounded(input.level.as_deref()),
90+
driver: bounded(input.driver.as_deref()),
91+
criteria_statement: bounded(input.criteria_statement.as_deref()),
92+
horizon: bounded(input.horizon.as_deref()),
93+
non_scope: bounded(input.non_scope.as_deref()),
94+
weekly_commitment: bounded(input.weekly_commitment.as_deref()),
95+
obstacles: bounded(input.obstacles.as_deref()),
96+
}
97+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
//! commands_goals_lifecycle 单测(毕业仪式守卫矩阵/回顾流/放弃/档案——内存库)。
22
3+
use crate::commands_goals::create_goal_inner;
4+
use crate::commands_goals::intent::{GoalCreateInput, GoalMilestoneInput};
35
use crate::commands_goals::milestones::set_goal_milestone_status_inner;
4-
use crate::commands_goals::{create_goal_inner, GoalCreateInput, GoalMilestoneInput};
56
use crate::commands_goals_lifecycle::{
67
goal_abandon_inner, goal_retro_inner, goal_settle_inner, list_goal_graduations_inner,
78
};

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! commands_goals_plan 单测(确认流落库/体系链接/概念弱信号——内存库)。
22
3-
use crate::commands_goals::{create_goal_inner, GoalCreateInput};
3+
use crate::commands_goals::create_goal_inner;
4+
use crate::commands_goals::intent::GoalCreateInput;
45
use crate::db::Db;
56
use crate::goal_schema::NewMilestone;
67
use crate::types::NewNoteGroup;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
//! commands_goals 命令层单测(inner 纯编排——内存库 AAA 模式)。
22
3+
use crate::commands_goals::intent::{GoalCreateInput, GoalMilestoneInput};
34
use crate::commands_goals::milestones::{
45
add_goal_milestone_inner, bind_goal_group_inner, set_goal_milestone_status_inner,
56
unbind_goal_group_inner,
67
};
78
use crate::commands_goals::views::{get_goal_detail_inner, get_goal_progress_inner, list_goals_inner};
89
use crate::commands_goals::{
910
create_goal_inner, delete_goal_inner, require_goal, update_goal_inner,
10-
update_goal_interview_inner, update_goal_status_inner, GoalCreateInput, GoalMilestoneInput,
11+
update_goal_interview_inner, update_goal_status_inner,
1112
};
1213
use crate::db::Db;
1314
use crate::goal_schema::{TIER_HANDS_ON, TIER_SOLO_PROJECT};

0 commit comments

Comments
 (0)