This page is a reference for evaluators and library users who need to know
whether trueppm-scheduler models their schedule the way they expect. Each row is
one rule the engine implements today, with the issue or ADR that decided it.
Rows marked Differs are the ones where a schedule built in MS Project or
Primavera P6 can come out differently here. Everything else follows the
conventions those tools use.
Lag counts calendar days. The resulting date snaps forward to the successor’s next working day (backward for negative lag), so a weekend can absorb a short lag.
Differs — MS Project counts lag in working time by default and treats elapsed days (2ed) as the opt-in. P6 counts lag on a working calendar.
All four link types (FS, SS, FF, SF) take a lead or lag.
Same.
—
A task driven by an FF or SF link stays contiguous and is right-aligned on its pinned finish, so its start moves back. As a result, CPM is not monotone in duration on a network with FF/SF links: a longer task can start earlier and pull an SS successor with it.
An SF link finishes the successor at the start of the predecessor’s start day. With zero lag, the successor’s last working day is the day before. The rule is the same for a task predecessor and a milestone predecessor.
A task whose links are all SF is placed by them alone, even before the project start: a predecessor on the project’s first working day puts a zero-lag SF successor on the working day before it. Only the project-start floor is lifted. The data date, a planned_start, and a recorded actual_start still hold the task. Any FS, SS or FF link on the task keeps the project-start floor too, so leads stay floored. Adding a non-binding FS, SS or FF link to an SF-only task can therefore move it later, back to the project start.
Same as MS Project, which schedules the SF successor ahead of the project start.
A zero-duration milestone is an instant. A milestone driven by work sits at the end of its driver’s finish day, and a milestone held by a floor sits at the start of that day. When a lag, or a predecessor’s recorded finish on a non-working day, places the milestone after a weekend or holiday, it is shown at the start of the next working day. The project start only bounds where a milestone is shown. When a lead or an SF-only predecessor puts a milestone’s links before the project start, the milestone is shown on the project’s first working day, but its successors’ lags still count from the earlier point, so the project-start floor alone never makes inserting a milestone into a finish-to-start link move anything. The data date, a planned_start, and a recorded actual_start still hold the milestone and every lag measured from it, so a milestone held by one of those can move a successor later than the same link without it. A milestone held at the project start keeps zero total float and reads as critical, even when the tasks on both sides of it have slack.
Same for the instant and the weekend rule: MS Project moves an elapsed lag that ends on non-working time to the next working time. Unverified for the project-start rule (#4225): passing the earlier point on to a floored milestone’s successors has not been checked against MS Project or P6.
The only date constraint is start-no-earlier-than, via Task.planned_start, snapped to the next working day. planned_finish is reserved and inert: there is no deadline, finish constraint, must-start-on, or as-late-as-possible.
Differs — MS Project has eight constraint types plus deadlines. P6 has primary and secondary constraints.
A completed task (actual_finish set, or percent_complete at 100) is pinned to its recorded dates verbatim, even when they fall on a non-working day. It is out of network logic and is never re-sampled in Monte Carlo.
An in-progress task schedules only its remaining work, duration − floor(duration × percent_complete / 100) working days. That work runs forward from the data date (status_date) and is floored at actual_start, which is kept unsnapped.
total_float is the working-day span from the early start to the late start, and is_critical means exactly total_float == 0.
—
—
Float stops where a slip would move the shown finish. When the project ends at the end of a day (work finishing Friday), a milestone placed at the start of a day, by a start-to-start link from work or by a floor, can slip only to the start of that Friday: one more day puts it at the start of Monday and moves project_finish. A finish-to-start predecessor places the same milestone at the end of a day, so it keeps its float up to the finish.
free_float inverts the forward constraint across all four link types, anchored on each successor’s early date, and is capped at total float. A task with no live successor falls back to its total float, and completed successors are skipped. A slip that leaves a milestone on the same midnight but changes the edge of the day it is shown on still moves it: when a milestone sits at the end of Wednesday and a start-to-start link would place it at the start of Thursday, the same instant, free float through that link stops a day short.
ScheduleResult.project_finish and a milestone’s early_finish are the day the finish is shown on, not an instant, and milestone_at_day_end says which edge of that day it is. The end of a Friday and the start of the following Monday are the same point in working time, so the shown day can move across a weekend or holiday while the working-time finish does not. To measure a slip, compare in working time and read a start-of-day milestone finish as the end of the working day before it. Never subtract two shown days. TruePPM measures a finish shift this way in the project end-date shift notification, the activity feed, the milestone forecast digest and sprint bridge card, the Monte Carlo what-if, the cross-project sprint-boundary check, baseline drift (the activity feed, the overview’s “Slipped vs baseline” list, a task’s Baseline tab deltas and a closed sprint’s milestone slip), the milestone rollup variance, the program rollup’s baseline and schedule variance, and every Monte Carlo delta — each percentile against the CPM finish (delta_vs_cpm), added time (risk_premium_days), run to run in the forecast history, and the what-if. A baseline or Monte Carlo run recorded before TruePPM kept which edge of the day each finish sat on is read as the end of its day. A task’s own schedule_variance_days (actual finish vs baseline) and baseline_finish_variance_days (forecast finish vs baseline — the baseline chip on a board card and in the task drawer) are measured the same way (#4203). Monte Carlo percentiles carry the same reading (#4204).
A three-point estimate samples a Beta distribution, fitted by method of moments to the classic PERT mean (O + 4M + P) / 6 and σ (P − O) / 6. This is not the λ=4 Beta-PERT. Both have the same mean, but on a symmetric estimate the band here is about 12% narrower, so P80 and P95 land slightly earlier. On an estimate whose most-likely value sits at one end, the band is wider.
Differs from the λ=4 Beta-PERT that @RISK and Primavera Risk Analysis default to. Neither MS Project nor P6 samples on its own.
Every sampled duration is floored at the task’s own duration, so the optimistic tail below the plan is not expressed. To model finishing early, lower duration.
No percentile precedes the CPM finish on a network built only from FS and SS links. On an FF/SF network a percentile can land earlier, because CPM itself is non-monotone there (see the FF/SF row above).
P50/P80/P95 are ranked by each run’s working-time finish. Where runs at the same position are shown on different days (the end of a Friday, the start of the Monday after it), each is shown on the day its rank lands on with runs ordered by position and then shown day, so it stays a quantile of the distribution in both directions, with p50_at_day_start / p80_at_day_start / p95_at_day_start saying which edge of that day each one is. A run whose finish is a start-of-day milestone ranks level with a run ending the Friday before it. Compare a percentile with the CPM finish, or with another run’s, in working time.
A fixed seed reproduces P50/P80/P95 exactly on the same numpy version and the same trueppm-scheduler version. Upgrading either one can move seeded percentiles.