scheduler: SF anchor's positive lag is absorbed by an adjacent weekend when the pre-snap anchor sits on the working day before it
Summary
_edge_anchor's SF branch (packages/scheduler/src/trueppm_scheduler/engine.py)
pre-snaps its anchor to the previous working day (_prev_working_day(instant - 1, pred_cal)) before the caller adds lag, for both ordinary-work SF and a
milestone predecessor's SF link. This is the exact bug class #4273 (closed) already fixed
for FF-out-of-a-milestone-with-positive-lag ("Anchoring a positive lag on the last
working day as well ... let a weekend absorb it") — but the analogous fix (defer
the snap, anchor on the raw instant - 1 instead, when lag > 0) was only applied
to the FF branch, not to SF.
Repro (found while building #4327's fixture)
Two tasks, 1-day duration each, project calendar Mon-Fri, 11 -SF(lag=2d)-> 12,
project start Monday:
from datetime import date, timedelta
from trueppm_scheduler import Calendar, Dependency, DependencyType, Project, Task, schedule
tasks = [Task(id="11", name="T1", duration=timedelta(days=1)),
Task(id="12", name="T2", duration=timedelta(days=1))]
deps = [Dependency("11", "12", dep_type=DependencyType.SF, lag=timedelta(days=2))]
project = Project(id="x", name="x", start_date=date(2016, 2, 8), # Monday
calendar=Calendar(), tasks=tasks, dependencies=deps)
result = schedule(project)- Engine: task 12
early_start == early_finish == 2016-02-08(same day as task 11 — the 2-day lag had no effect). - MS Project (verified against
task-links-project2016-mpp12.mppandtask-links-project2016-mpp14.mpp, two independently-saved files from MPXJ's generated cross-version test matrix,joniles/mpxj): task 12 finishes 2016-02-09 (Tuesday) — one working day later, per the 2-working-day lag.
The mismatch only appears when the zero-lag SF anchor (the working day before the
predecessor's start) sits immediately before a weekend — here, predecessor starts
Monday, so the zero-lag anchor is the prior Friday, and Friday + 2 calendar days
lands on Sunday, snapped forward back to Monday, absorbing the entire lag.
When the predecessor starts mid-week (Wed, Thu — also checked against real
MS-Project-saved files, task-links-project2000-mpp8/mpp9 and
task-links-project2019-mpp12/mpp14) the engine's current behavior already
matches MS Project, because no weekend sits between the pre-snapped anchor and
the lagged result.
Where to look
_edge_anchor(engine.py): theelif dep_type == DependencyType.FF and lag > timedelta(0): return _safe_offset(instant, -_ONE_DAY)special case needs an SF equivalent — and the ordinary-work SF path (instant = startthen falling through to the shared pre-snapped tail) needs the same treatment, since it never passes through that milestone-onlyelifat all._sf_latest_startand_milestone_latest's finish-anchored branch: the backward pass's SF inverse is stated in terms of the sameprev_wd(start - 1)forward anchor ("deliberately the same expression as_milestone_latest's finish-anchored branch"), so fixing the forward anchor without checking the backward pass would desync late-start/float for exactly the cases this bug affects.- Rust/WASM conformance (
packages/wasm-scheduler,forward.rs) and Monte Carlo padding need the same fix in lockstep per the project's engine-conformance requirement, orwasm:conformanceshould start failing once a fixture exercises this.
Acceptance
- Fix (or explicitly scope-limit and document as
Differs) the SF anchor's interaction with positive lag when the zero-lag anchor sits adjacent to a weekend, for both ordinary-work and milestone-predecessor SF. - Verify the backward pass (
_sf_latest_start,_milestone_latest) stays consistent with whatever the forward anchor becomes. - Add a regression test reproducing the Monday-start case above (no existing
test exercises it — the two
task_links_cross_version.pyscenarios #4327 added were deliberately chosen to match today's engine so #4327's branch could verify the composition of SF+lag, not this specific weekend-adjacency defect). - Rust/WASM conformance check for the same case.
Related
#4327 (discovered while building its fixture), #4273 (closed) (the analogous fix for FF-out-of-a-milestone), #2534 (closed)/#2535 (the broader, separate open question of calendar-day vs. working-day lag counting — this bug is narrower and does not depend on how that question is resolved).