1use std::collections::{HashMap, HashSet, VecDeque};
45use std::fmt;
46use std::sync::Arc;
47use std::sync::atomic::{AtomicBool, Ordering};
48
49#[path = "study/command.rs"]
50mod command;
51#[path = "study/display.rs"]
52mod display;
53#[path = "study/error.rs"]
54mod error;
55#[path = "study/phase.rs"]
56mod phase;
57#[path = "study/plan.rs"]
58mod plan;
59#[path = "study/record.rs"]
60mod record;
61#[path = "study/renderer.rs"]
62mod renderer;
63#[path = "study/scheduler.rs"]
64mod scheduler;
65#[path = "study/task.rs"]
66mod task;
67#[path = "study/timing.rs"]
68mod timing;
69#[path = "study/tui.rs"]
70mod tui;
71
72pub use error::StudyError;
73pub use phase::{
74 Phase, PhaseBuilder, PhaseFailurePolicy, PhaseId, Task, TaskId, TaskKey, TaskMode, TaskSelector,
75};
76pub use plan::StudyPlan;
77pub use record::{PhaseRecord, StudyRecord, TaskRecord};
78pub use renderer::{CancellationToken, ProgressSummary, TaskIdentity, TaskStatus};
79pub use task::{TaskContext, TaskResult};
80
81use display::DisplayMode;
82use renderer::StudyRenderer;
83
84static STUDY_OWNED: AtomicBool = AtomicBool::new(false);
85
86type SatisfiedPhaseVerifier = Arc<dyn Fn(PhaseId) -> bool + Send + Sync + 'static>;
87
88pub struct StudyBuilder {
90 phases: Vec<Phase>,
91 output: DisplayMode,
92 satisfied_phase: Option<SatisfiedPhaseVerifier>,
93 record_path: std::path::PathBuf,
94}
95
96impl StudyBuilder {
97 pub fn phase(mut self, phase: Phase) -> Self {
99 self.phases.push(phase);
100 self
101 }
102
103 pub fn phases<I>(mut self, phases: I) -> Self
105 where
106 I: IntoIterator<Item = Phase>,
107 {
108 self.phases.extend(phases);
109 self
110 }
111
112 pub fn satisfied_phase_verifier<F>(mut self, verifier: F) -> Self
114 where
115 F: Fn(PhaseId) -> bool + Send + Sync + 'static,
116 {
117 self.satisfied_phase = Some(Arc::new(verifier));
118 self
119 }
120
121 pub fn automatic(mut self) -> Self {
123 self.output = DisplayMode::Auto;
124 self
125 }
126
127 pub fn terminal(mut self) -> Self {
129 self.output = DisplayMode::Terminal;
130 self
131 }
132
133 pub fn plain(mut self) -> Self {
135 self.output = DisplayMode::Plain;
136 self
137 }
138
139 pub fn hidden(mut self) -> Self {
141 self.output = DisplayMode::Hidden;
142 self
143 }
144
145 pub fn build(self) -> Result<Study, StudyError> {
147 validate_plan(&self.phases)?;
148 Ok(Study {
149 phases: self.phases,
150 output: self.output,
151 satisfied_phase: self.satisfied_phase,
152 cancellation: CancellationToken::new(),
153 record_path: self.record_path,
154 })
155 }
156}
157
158impl fmt::Debug for StudyBuilder {
159 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160 formatter
161 .debug_struct("StudyBuilder")
162 .field("phases", &self.phases.len())
163 .field("output", &self.output)
164 .field("record_path", &self.record_path)
165 .field(
166 "has_satisfied_phase_verifier",
167 &self.satisfied_phase.is_some(),
168 )
169 .finish_non_exhaustive()
170 }
171}
172
173pub struct Study {
175 phases: Vec<Phase>,
176 output: DisplayMode,
177 satisfied_phase: Option<SatisfiedPhaseVerifier>,
178 cancellation: CancellationToken,
179 record_path: std::path::PathBuf,
180}
181
182impl Study {
183 pub fn builder(record_path: impl Into<std::path::PathBuf>) -> StudyBuilder {
185 StudyBuilder {
186 phases: Vec::new(),
187 output: DisplayMode::Auto,
188 satisfied_phase: None,
189 record_path: record_path.into(),
190 }
191 }
192
193 pub fn phases(&self) -> &[Phase] {
195 &self.phases
196 }
197
198 pub fn plan(&self) -> StudyPlan {
201 StudyPlan::from_phases(&self.phases)
202 }
203
204 pub fn write_plan_json(&self, path: impl AsRef<std::path::Path>) -> Result<(), StudyError> {
208 self.plan().write_json(path)
209 }
210
211 pub fn phase(&self, id: impl Into<PhaseId>) -> Option<&Phase> {
213 let id = id.into();
214 self.phases.iter().find(|phase| phase.id() == id)
215 }
216
217 pub fn cancellation_token(&self) -> CancellationToken {
219 self.cancellation.clone()
220 }
221
222 pub fn unique_task_matching(&self, selector: &TaskSelector) -> Result<&Task, StudyError> {
224 let mut matches = self
225 .phases
226 .iter()
227 .flat_map(|phase| phase.tasks())
228 .filter(|task| selector.matches(task));
229 let first = matches.next().ok_or_else(|| StudyError::TaskNotFound {
230 selector: selector.to_string(),
231 })?;
232 if let Some(second) = matches.next() {
233 return Err(StudyError::TaskSelectorAmbiguous {
234 selector: selector.to_string(),
235 first: first.key().to_string(),
236 second: second.key().to_string(),
237 });
238 }
239 Ok(first)
240 }
241
242 pub fn run(self) -> Result<StudySummary, StudyError> {
244 let selected = topological_positions(&self.phases)?;
245 self.execute(&selected)
246 }
247
248 pub fn run_phases<I, P>(self, phases: I) -> Result<StudySummary, StudyError>
250 where
251 I: IntoIterator<Item = P>,
252 P: Into<PhaseId>,
253 {
254 let selected = self.select_phases(phases, false)?;
255 self.execute(&selected)
256 }
257
258 pub fn run_phases_with_dependencies<I, P>(self, phases: I) -> Result<StudySummary, StudyError>
260 where
261 I: IntoIterator<Item = P>,
262 P: Into<PhaseId>,
263 {
264 let selected = self.select_phases(phases, true)?;
265 self.execute(&selected)
266 }
267
268 fn select_phases<I, P>(
269 &self,
270 phases: I,
271 include_dependencies: bool,
272 ) -> Result<Vec<usize>, StudyError>
273 where
274 I: IntoIterator<Item = P>,
275 P: Into<PhaseId>,
276 {
277 let mut selected = selected_ids(&self.phases, phases)?;
278 if include_dependencies {
279 let positions: HashMap<_, _> = self
280 .phases
281 .iter()
282 .enumerate()
283 .map(|(position, phase)| (phase.id(), position))
284 .collect();
285 let mut pending: Vec<_> = selected.iter().copied().collect();
286 while let Some(id) = pending.pop() {
287 let phase = &self.phases[positions[&id]];
288 for dependency in phase.dependencies() {
289 if !self.is_satisfied(*dependency) && selected.insert(*dependency) {
290 pending.push(*dependency);
291 }
292 }
293 }
294 } else {
295 for phase in self
296 .phases
297 .iter()
298 .filter(|phase| selected.contains(&phase.id()))
299 {
300 for dependency in phase.dependencies() {
301 if !selected.contains(dependency) && !self.is_satisfied(*dependency) {
302 return Err(StudyError::UnsatisfiedPhaseDependency {
303 phase: phase.id().get(),
304 dependency: dependency.get(),
305 });
306 }
307 }
308 }
309 }
310 Ok(topological_positions(&self.phases)?
311 .into_iter()
312 .filter(|position| selected.contains(&self.phases[*position].id()))
313 .collect())
314 }
315
316 fn is_satisfied(&self, phase: PhaseId) -> bool {
317 self.satisfied_phase
318 .as_ref()
319 .is_some_and(|verify| verify(phase))
320 }
321
322 fn execute(self, selected: &[usize]) -> Result<StudySummary, StudyError> {
323 let _lease = StudyLease::acquire()?;
324 let total_phases = selected.len();
325 let total_tasks = selected
326 .iter()
327 .map(|position| self.phases[*position].tasks().len())
328 .sum();
329 let execution = {
330 let selected_phases = selected
331 .iter()
332 .map(|position| &self.phases[*position])
333 .collect::<Vec<_>>();
334 record::StudyRecorder::start(self.record_path.clone(), &selected_phases)?
335 };
336 let mut summaries = Vec::with_capacity(total_phases);
337 let mut phases = self.phases.into_iter().map(Some).collect::<Vec<_>>();
338
339 for (selection_position, phase_position) in selected.iter().copied().enumerate() {
340 let phase = phases[phase_position]
341 .take()
342 .expect("selected phase positions are unique");
343 execution.phase_started(phase.id())?;
344 display::phase_start(self.output, &phase, selection_position + 1, total_phases);
345 let heading = display::phase_heading(&phase, selection_position + 1, total_phases);
346 let builder = StudyRenderer::for_phase(&phase, &heading)?
347 .cancellation_token(self.cancellation.clone());
348 let renderer = match self.output {
349 DisplayMode::Auto => builder,
350 DisplayMode::Terminal => builder.terminal(),
351 DisplayMode::Plain => builder.plain(),
352 DisplayMode::Hidden => builder.hidden(),
353 }
354 .start()?;
355 let phase_id = phase.id();
356 let phase_label: Arc<str> = phase.label().into();
357 let require_confirm = phase.requires_confirmation();
358 let result = scheduler::execute_phase(phase, &renderer, &execution);
359 let task_execution = renderer.task_execution_snapshots();
360 let progress = if result.is_ok() {
361 renderer.complete(format!("phase {phase_id} completed"))?
362 } else {
363 renderer.fail(format!("phase {phase_id} failed"))?
364 };
365 let success = result.is_ok() && progress.is_success();
366 execution.phase_finished(phase_id, success, &progress, task_execution)?;
367 display::phase_complete(self.output, phase_id, &phase_label, success);
368 summaries.push(PhaseSummary {
369 id: phase_id,
370 label: phase_label,
371 progress,
372 });
373 if let Err(error) = result {
374 return Err(Self::fail_phase_with_summary(
375 self.output,
376 &execution,
377 summaries,
378 total_tasks,
379 error,
380 )?);
381 }
382 if require_confirm && selection_position + 1 < total_phases {
383 let next = phases[selected[selection_position + 1]]
384 .as_ref()
385 .expect("the next selected phase has not executed");
386 let confirmed = match display::confirm_transition(phase_id, next) {
387 Ok(confirmed) => confirmed,
388 Err(source) => {
389 return Err(Self::fail_phase_with_summary(
390 self.output,
391 &execution,
392 summaries,
393 total_tasks,
394 StudyError::PhaseConfirmationInput {
395 phase: phase_id.get(),
396 source,
397 },
398 )?);
399 }
400 };
401 if !confirmed {
402 return Err(Self::fail_phase_with_summary(
403 self.output,
404 &execution,
405 summaries,
406 total_tasks,
407 StudyError::PhaseConfirmationEof {
408 phase: phase_id.get(),
409 },
410 )?);
411 }
412 }
413 }
414
415 display::study_complete(self.output, summaries.len(), total_tasks, true);
416 let record = execution.finish(true)?;
417 Ok(StudySummary {
418 phases: summaries.into(),
419 record: Box::new(record),
420 })
421 }
422
423 fn fail_phase_with_summary(
424 output: DisplayMode,
425 execution: &record::StudyRecorder,
426 summaries: Vec<PhaseSummary>,
427 total_tasks: usize,
428 source: StudyError,
429 ) -> Result<StudyError, StudyError> {
430 display::study_complete(output, summaries.len(), total_tasks, false);
431 let record = execution.finish(false)?;
432 Ok(StudyError::PhaseExecutionFailed {
433 summary: StudySummary {
434 phases: summaries.into(),
435 record: Box::new(record),
436 },
437 source: Box::new(source),
438 })
439 }
440}
441
442impl fmt::Debug for Study {
443 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
444 formatter
445 .debug_struct("Study")
446 .field("phases", &self.phases.len())
447 .field(
448 "tasks",
449 &self.phases.iter().map(|p| p.tasks().len()).sum::<usize>(),
450 )
451 .field("output", &self.output)
452 .finish_non_exhaustive()
453 }
454}
455
456#[derive(Clone, Debug, Eq, PartialEq)]
458pub struct PhaseSummary {
459 id: PhaseId,
460 label: Arc<str>,
461 progress: ProgressSummary,
462}
463
464impl PhaseSummary {
465 pub const fn id(&self) -> PhaseId {
467 self.id
468 }
469
470 pub fn label(&self) -> &str {
472 &self.label
473 }
474
475 pub fn progress(&self) -> &ProgressSummary {
477 &self.progress
478 }
479
480 pub fn is_success(&self) -> bool {
482 self.progress.is_success()
483 }
484}
485
486#[derive(Clone, Debug, Eq, PartialEq)]
488pub struct StudySummary {
489 phases: Arc<[PhaseSummary]>,
490 record: Box<StudyRecord>,
491}
492
493impl StudySummary {
494 pub fn phases(&self) -> &[PhaseSummary] {
496 &self.phases
497 }
498
499 pub fn record(&self) -> &StudyRecord {
501 self.record.as_ref()
502 }
503
504 pub fn total_tasks(&self) -> u64 {
506 self.phases.iter().map(|phase| phase.progress.total()).sum()
507 }
508
509 pub fn is_success(&self) -> bool {
511 !self.phases.is_empty() && self.phases.iter().all(PhaseSummary::is_success)
512 }
513}
514
515struct StudyLease;
516
517impl StudyLease {
518 fn acquire() -> Result<Self, StudyError> {
519 STUDY_OWNED
520 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
521 .map(|_| Self)
522 .map_err(|_| StudyError::TerminalAlreadyOwned)
523 }
524}
525
526impl Drop for StudyLease {
527 fn drop(&mut self) {
528 STUDY_OWNED.store(false, Ordering::Release);
529 }
530}
531
532fn selected_ids<I, P>(phases: &[Phase], requested: I) -> Result<HashSet<PhaseId>, StudyError>
533where
534 I: IntoIterator<Item = P>,
535 P: Into<PhaseId>,
536{
537 let known: HashSet<_> = phases.iter().map(Phase::id).collect();
538 let selected: HashSet<_> = requested.into_iter().map(Into::into).collect();
539 if selected.is_empty() {
540 return Err(StudyError::EmptyPhaseSet);
541 }
542 if let Some(unknown) = selected.iter().find(|id| !known.contains(id)) {
543 return Err(StudyError::UnknownSelectedPhase {
544 phase: unknown.get(),
545 });
546 }
547 Ok(selected)
548}
549
550fn validate_plan(phases: &[Phase]) -> Result<(), StudyError> {
551 if phases.is_empty() {
552 return Err(StudyError::EmptyPhaseSet);
553 }
554 let mut ids = HashSet::with_capacity(phases.len());
555 for phase in phases {
556 if !ids.insert(phase.id()) {
557 return Err(StudyError::DuplicatePhaseId {
558 phase: phase.id().get(),
559 });
560 }
561 }
562 for phase in phases {
563 for dependency in phase.dependencies() {
564 if !ids.contains(dependency) {
565 return Err(StudyError::UnknownPhaseDependency {
566 phase: phase.id().get(),
567 dependency: dependency.get(),
568 });
569 }
570 }
571 for task in phase.tasks() {
572 if !task.has_workload() && !task.is_completed() {
573 return Err(StudyError::MissingTaskWorkload {
574 task: task.key().to_string(),
575 });
576 }
577 }
578 }
579 topological_positions(phases).map(|_| ())
580}
581
582fn topological_positions(phases: &[Phase]) -> Result<Vec<usize>, StudyError> {
583 let positions: HashMap<_, _> = phases
584 .iter()
585 .enumerate()
586 .map(|(position, phase)| (phase.id(), position))
587 .collect();
588 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); phases.len()];
589 let mut indegree = vec![0_usize; phases.len()];
590
591 for (position, phase) in phases.iter().enumerate() {
592 for dependency in phase.dependencies() {
593 let dependency_position = positions[dependency];
594 dependents[dependency_position].push(position);
595 indegree[position] += 1;
596 }
597 }
598
599 let mut queue = VecDeque::new();
600 for (position, degree) in indegree.iter().enumerate() {
601 if *degree == 0 {
602 queue.push_back(position);
603 }
604 }
605
606 let mut ordered = Vec::with_capacity(phases.len());
607 while let Some(position) = queue.pop_front() {
608 ordered.push(position);
609 for dependent in dependents[position].drain(..) {
610 indegree[dependent] -= 1;
611 if indegree[dependent] == 0 {
612 queue.push_back(dependent);
613 }
614 }
615 }
616
617 if ordered.len() != phases.len() {
618 let phase_position = indegree
619 .iter()
620 .position(|degree| *degree > 0)
621 .expect("cyclic dependency must leave at least one phase with indegree");
622 return Err(StudyError::PhaseDependencyCycle {
623 phase: phases[phase_position].id().get(),
624 });
625 }
626
627 Ok(ordered)
628}