ito_core/templates/
review.rs1use super::{
2 PeerReviewContext, ReviewAffectedSpecInfo, ReviewArtifactInfo, ReviewTaskSummaryInfo,
3 ReviewTestingPolicy, ReviewValidationIssueInfo, TemplatesError, artifact_done,
4 default_schema_name, load_composed_user_guidance, read_change_schema, resolve_schema,
5 validate_change_name_input,
6};
7use crate::change_repository::FsChangeRepository;
8use crate::module_repository::FsModuleRepository;
9use crate::show::{parse_change_show_json, read_change_delta_spec_files};
10use crate::validate::validate_change;
11use chrono::{SecondsFormat, Utc};
12use ito_common::paths;
13use ito_config::{ConfigContext, load_cascading_project_config};
14use std::collections::BTreeSet;
15use std::path::Path;
16
17pub fn compute_review_context(
19 ito_path: &Path,
20 change: &str,
21 schema_name: Option<&str>,
22 ctx: &ConfigContext,
23) -> Result<PeerReviewContext, TemplatesError> {
24 if !validate_change_name_input(change) {
25 return Err(TemplatesError::InvalidChangeName);
26 }
27
28 let chosen_schema = match schema_name {
29 Some(s) if !s.trim().is_empty() => s.trim().to_string(),
30 _ => read_change_schema(ito_path, change),
31 };
32
33 let resolved = resolve_schema(Some(&chosen_schema), ctx)?;
34 let change_dir = paths::change_dir(ito_path, change);
35 if !change_dir.exists() {
36 return Err(TemplatesError::ChangeNotFound(change.to_string()));
37 }
38
39 let change_repo = FsChangeRepository::new(ito_path);
40 let change_data = change_repo
41 .get(change)
42 .map_err(|_| TemplatesError::ChangeNotFound(change.to_string()))?;
43
44 let mut artifacts = Vec::new();
45 for artifact in &resolved.schema.artifacts {
46 artifacts.push(ReviewArtifactInfo {
47 id: artifact.id.clone(),
48 path: change_dir
49 .join(&artifact.generates)
50 .to_string_lossy()
51 .to_string(),
52 present: artifact_done(&change_dir, &artifact.generates),
53 });
54 }
55
56 let (validation_passed, validation_issues) =
57 match validate_change(&change_repo, ito_path, change, false) {
58 Ok(report) => {
59 let mut issues = Vec::new();
60 for issue in report.issues {
61 issues.push(ReviewValidationIssueInfo {
62 level: issue.level,
63 path: issue.path,
64 message: issue.message,
65 line: issue.line,
66 column: issue.column,
67 });
68 }
69 (report.valid, issues)
70 }
71 Err(err) => (
72 false,
73 vec![ReviewValidationIssueInfo {
74 level: "error".to_string(),
75 path: paths::change_dir(ito_path, change)
76 .to_string_lossy()
77 .to_string(),
78 message: err.to_string(),
79 line: None,
80 column: None,
81 }],
82 ),
83 };
84
85 let wave_count = {
86 let distinct_waves: BTreeSet<u32> = change_data
87 .tasks
88 .tasks
89 .iter()
90 .filter_map(|task| task.wave)
91 .collect();
92 if distinct_waves.is_empty() {
93 change_data.tasks.waves.len()
94 } else {
95 distinct_waves.len()
96 }
97 };
98
99 let task_summary = if change_data.tasks.progress.total == 0 {
100 None
101 } else {
102 Some(ReviewTaskSummaryInfo {
103 total: change_data.tasks.progress.total,
104 complete: change_data.tasks.progress.complete,
105 in_progress: change_data.tasks.progress.in_progress,
106 pending: change_data.tasks.progress.pending,
107 shelved: change_data.tasks.progress.shelved,
108 wave_count,
109 })
110 };
111
112 let mut affected_specs = Vec::new();
113 if let Ok(delta_files) = read_change_delta_spec_files(&change_repo, change) {
114 let show = parse_change_show_json(change, &delta_files);
115 for delta in show.deltas {
116 if delta.operation != "MODIFIED" {
117 continue;
118 }
119
120 let description = if delta.description.trim().is_empty() {
121 None
122 } else {
123 Some(delta.description)
124 };
125
126 affected_specs.push(ReviewAffectedSpecInfo {
127 spec_id: delta.spec,
128 operation: delta.operation,
129 description,
130 });
131 }
132 }
133
134 let module_id = change_data.module_id.clone();
135 let module_name = if let Some(id) = module_id.as_deref() {
136 FsModuleRepository::new(ito_path)
137 .get(id)
138 .ok()
139 .map(|module| module.name)
140 } else {
141 None
142 };
143
144 let user_guidance = load_composed_user_guidance(ito_path, "review").unwrap_or(None);
145 let testing_policy = read_testing_policy(ito_path, ctx);
146
147 Ok(PeerReviewContext {
148 change_name: change.to_string(),
149 change_dir: change_dir.to_string_lossy().to_string(),
150 schema_name: if resolved.schema.name.trim().is_empty() {
151 default_schema_name().to_string()
152 } else {
153 resolved.schema.name
154 },
155 module_id,
156 module_name,
157 artifacts,
158 validation_issues,
159 validation_passed,
160 task_summary,
161 affected_specs,
162 user_guidance,
163 testing_policy,
164 generated_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
165 })
166}
167
168fn read_testing_policy(ito_path: &Path, ctx: &ConfigContext) -> ReviewTestingPolicy {
169 let project_root = ctx
170 .project_dir
171 .as_deref()
172 .unwrap_or_else(|| ito_path.parent().unwrap_or(ito_path));
173 let merged_config = load_cascading_project_config(project_root, ito_path, ctx).merged;
174 let tdd_workflow = merged_config
175 .get("defaults")
176 .and_then(|v| v.get("testing"))
177 .and_then(|v| v.get("tdd"))
178 .and_then(|v| v.get("workflow"))
179 .and_then(serde_json::Value::as_str)
180 .unwrap_or("red-green-refactor")
181 .to_string();
182 let coverage_target_percent = merged_config
183 .get("defaults")
184 .and_then(|v| v.get("testing"))
185 .and_then(|v| v.get("coverage"))
186 .and_then(|v| v.get("target_percent"))
187 .and_then(serde_json::Value::as_u64)
188 .unwrap_or(80);
189
190 ReviewTestingPolicy {
191 tdd_workflow,
192 coverage_target_percent,
193 }
194}