ito_core/templates/
review.rs

1use 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
17/// Build the context payload used by `agent/review.md.j2`.
18pub 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}