Skip to content

Commit fc781f8

Browse files
alanbldclaude
andcommitted
fix: Include milestones in critical path calculation
Milestones (tasks with zero duration) were incorrectly excluded from the critical path due to a `duration_days > 0` check. Before: `is_critical: node.slack == 0 && node.duration_days > 0` After: `is_critical: node.slack == 0` This affected both: - The `is_critical` flag on ScheduledTask - The `critical_path` vector in Schedule Milestones with zero slack are now correctly marked as critical and included in the critical path output. Fixes the CTL PIII bug where "[7.6] TTG Release" milestone had 0 slack but wasn't shown as critical. Tests: Added `milestone_can_be_critical` regression test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8c80354 commit fc781f8

File tree

2 files changed

+36
-7
lines changed

2 files changed

+36
-7
lines changed

Cargo.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ members = [
1414
]
1515

1616
[workspace.package]
17-
version = "0.9.2"
17+
version = "0.9.3"
1818
edition = "2021"
1919
rust-version = "1.75"
2020
license = "MIT OR Apache-2.0"
@@ -24,10 +24,10 @@ categories = ["command-line-utilities", "development-tools"]
2424

2525
[workspace.dependencies]
2626
# Internal crates (version required for crates.io publishing)
27-
utf8proj-core = { version = "0.9.2", path = "crates/utf8proj-core" }
28-
utf8proj-parser = { version = "0.9.2", path = "crates/utf8proj-parser" }
29-
utf8proj-solver = { version = "0.9.2", path = "crates/utf8proj-solver" }
30-
utf8proj-render = { version = "0.9.2", path = "crates/utf8proj-render" }
27+
utf8proj-core = { version = "0.9.3", path = "crates/utf8proj-core" }
28+
utf8proj-parser = { version = "0.9.3", path = "crates/utf8proj-parser" }
29+
utf8proj-solver = { version = "0.9.3", path = "crates/utf8proj-solver" }
30+
utf8proj-render = { version = "0.9.3", path = "crates/utf8proj-render" }
3131

3232
# Date/Time
3333
chrono = { version = "0.4", features = ["serde"] }

crates/utf8proj-solver/src/lib.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3105,9 +3105,10 @@ impl Scheduler for CpmSolver {
31053105
.map(|(i, id)| (id, i))
31063106
.collect();
31073107

3108+
// Critical path includes all tasks with zero slack (including milestones)
31083109
let mut critical_path: Vec<TaskId> = nodes
31093110
.iter()
3110-
.filter(|(_, node)| node.slack == 0 && node.duration_days > 0)
3111+
.filter(|(_, node)| node.slack == 0)
31113112
.map(|(id, _)| id.clone())
31123113
.collect();
31133114

@@ -3233,7 +3234,7 @@ impl Scheduler for CpmSolver {
32333234
duration: Duration::days(node.original_duration_days),
32343235
assignments,
32353236
slack: Duration::days(node.slack),
3236-
is_critical: node.slack == 0 && node.duration_days > 0,
3237+
is_critical: node.slack == 0, // Milestones can be critical too
32373238
early_start: working_day_cache.get(node.early_start),
32383239
early_finish: if node.duration_days > 0 {
32393240
working_day_cache.get(node.early_finish - 1)
@@ -3841,6 +3842,34 @@ mod tests {
38413842
assert_eq!(schedule.tasks["done"].start, schedule.tasks["done"].finish);
38423843
}
38433844

3845+
#[test]
3846+
fn milestone_can_be_critical() {
3847+
// Regression test: milestones with zero slack should be marked critical
3848+
// Previously, milestones were excluded from critical path due to duration_days > 0 check
3849+
let mut project = Project::new("Milestone Critical Test");
3850+
project.start = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
3851+
project.tasks = vec![
3852+
Task::new("work").effort(Duration::days(5)),
3853+
Task::new("release").milestone().depends_on("work"),
3854+
];
3855+
3856+
let solver = CpmSolver::new();
3857+
let schedule = solver.schedule(&project).unwrap();
3858+
3859+
// Both tasks should be critical (linear chain)
3860+
assert!(schedule.tasks["work"].is_critical, "work should be critical");
3861+
assert!(
3862+
schedule.tasks["release"].is_critical,
3863+
"milestone should be critical when it has zero slack"
3864+
);
3865+
3866+
// Critical path should include milestone
3867+
assert!(
3868+
schedule.critical_path.contains(&"release".to_string()),
3869+
"critical_path should include the milestone"
3870+
);
3871+
}
3872+
38443873
#[test]
38453874
fn nested_tasks_are_flattened() {
38463875
let mut project = Project::new("Nested");

0 commit comments

Comments
 (0)