Skip to content

fix(gantt): cascaded issues and the critical path keep their working-day duration - #11065

Merged
ArtyomSavchenko merged 3 commits into
hcengineering:developfrom
MichaelUray:fix/gantt-working-day-duration
Oct 13, 2026
Merged

ArtyomSavchenko merged 3 commits into
hcengineering:developfrom
MichaelUray:fix/gantt-working-day-duration

Conversation

@MichaelUray

@MichaelUray MichaelUray commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #11054, which listed working-day durations for cascaded successors as
out of scope. Builds on the working-day span helpers from #11060 (merged).

Problem

In working-days mode simulateCascade places a shifted issue's anchor on a working day
but moves its other end by the same number of calendar days, so the issue keeps its
calendar-day length: a two-day successor pushed from Mon–Tue to a Friday ends on
Saturday (B Fri 16–Sat 17 in the #11054 example), a three-working-day predecessor pulled
across a weekend grows to five working days, and a successor pushed onto the day before
a holiday ends on the holiday. This applies to all four relation kinds; only which end
is anchored differs. The critical path computes LS/LF the same way, so the latest start
of a predecessor that is rolled back across a weekend is too early and its slack too large.

Change

  • simulateCascade: with a calendar, a pushed or pulled issue keeps its length in
    working days (workingDaySpan, never below 1): the free end is dueForSpan /
    startForSpan of the anchored end, for FS/SS (start anchored) and FF/SF (due
    anchored) alike. The anchor, the FS gap floor and the violation check from
    feat(tracker): Gantt chart for the issue tracker #10992/fix(tracker): Gantt cascade — do not pull a predecessor back when its due falls on a non-working day #11054 are unchanged. Without a calendar the arithmetic is byte-identical.
  • computeCriticalPath: the forward and backward passes preserve the working-day span
    instead of the millisecond duration, so LS/LF match what the cascade would produce.
  • Bars longer than MAX_WORKING_SPAN_DAYS calendar days retain their exact calendar-day
    length when moved; the bounded working-day helpers are not allowed to silently shorten
    them.

A bar stored entirely on non-working days counts as one working day once it is moved.
This normalisation is intentionally lossy: for example, Fri–Sat and Sat–Sun both become
one working-day bars, so pushing and then undoing such a bar does not restore its original
dates.

Tests

Updated: the #11054 chain now ends B on Mon 19 (two working days) and C on Tue 20–Wed 21;
the multi-hop floor test uses a Monday primary so the floor still dominates the second hop;
the pull test uses a two-working-day predecessor and keeps its expectation; three
working-days tests assert the far end as well. New: push and pull across a weekend for
every relation kind (FS, SS, FF, SF), holiday step-over, weekend-only and inverted bars,
long bars beyond the span cap, legacy calendar-day length for push and pull with fixed
values, and working-calendar CPM coverage for every relation type, holidays, non-working
project finishes, and a non-working latest finish in the backward pass.

@ArtyomSavchenko

Copy link
Copy Markdown
Member

Hi @MichaelUray
Thanks for the contribution, please take a look at the following comments:

  1. Orphaned doc comment (should fix). In critical-path.ts, the new endFor and startFor are inserted between the existing JSDoc for computeCriticalPath and the function itself. That JSDoc now attaches to endFor. Move the helpers above the comment block.
  2. Possible silent truncation (verify). scheduler.ts already imports MAX_WORKING_SPAN_DAYS. If workingDaySpan, dueForSpan or startForSpan clamp to it, a very long bar would be shortened when pushed. Confirm the helpers don't clamp, or add a test with a long bar.
  3. Lossy normalisation, by design. Bars on non-working days lose length when moved (Fri–Sat becomes 1 working day; Sat–Sun becomes 1). A push followed by an undo won't restore the original dates. The PR documents this and it is probably acceptable; a changelog line would help.
  4. Critical-path coverage is thinner than the cascade's. Only FS is tested. Add one SS/FF/SF case, a holiday case, and a case where projectFinish falls on a non-working day. startForSpan called with a non-working newLF in the backward pass is the one edge I could not verify.
  5. Slack units. Slack stays in calendar milliseconds, so it includes weekends (the 4-day figure in the test). This was already the case and the commit message says so, but a UI labelling slack in working days could confuse.
  6. Inverted dates in calendar mode. The "never below 1" claim depends on the helper. A one-line test with start > due would lock it in.

In working-days mode simulateCascade placed a shifted issue's anchor on a
working day but moved its other end by the same number of calendar
days, so the issue kept its calendar-day length and could end on a
weekend or a holiday. A pushed successor and a pulled predecessor now
keep their length in working days (workingDaySpan, never below 1); the
free end is dueForSpan / startForSpan of the anchored end, for all four
relation kinds. The anchor, the FS gap floor and the violation check are
unchanged, and without a calendar the arithmetic is byte-identical.

Test updates: the chain push from hcengineering#11054 now ends B on Mon 19 (two
working days) and C on Tue 20 - Wed 21; the multi-hop floor test uses a
Monday primary so the floor still dominates the second hop; the pull
test uses a two-working-day predecessor and keeps its expectation.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
computeCriticalPath preserved each issue's duration in milliseconds, so
with a working-days calendar the latest start of a predecessor rolled
back across a weekend was too early and its slack too large. The forward
and backward passes now keep the working-day span (endFor / startFor via
dueForSpan / startForSpan) when a calendar is active, so LS/LF match what
the cascade would produce. Legacy mode keeps the millisecond duration;
slack stays in milliseconds.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
…tical-path coverage, doc placement

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
@MichaelUray
MichaelUray force-pushed the fix/gantt-working-day-duration branch from 03d1ca8 to f70a031 Compare October 13, 2026 12:12
@MichaelUray

Copy link
Copy Markdown
Contributor Author

Rebased onto current develop; the review fixes are in f70a031.

  1. Moved endFor and startFor above the computeCriticalPath JSDoc, so the comment again documents the exported function.

  2. Confirmed that the working-span helpers are capped. Cascades and CPM now detect bars beyond the cap and preserve their calendar-day length instead of silently shortening them; added a long-bar cascade test.

  3. Updated the PR body draft with the intentional lossy normalisation: non-working-only bars become one working-day bars when moved, so push then undo does not restore the original dates.

  4. Added working-calendar CPM coverage for SS, FF, and SF, a holiday-crossing FS relation, a non-working project finish, and an FF backward-pass case where startForSpan receives a non-working LF.

  5. Kept slack as calendar milliseconds and added an in-code comment that weekends are deliberately included. The sidebar's displayed day count therefore remains a calendar-day figure.

  6. Added a calendar-mode cascade test for an inverted stored bar; after a push it becomes a one-day bar, never a negative-duration bar.

@ArtyomSavchenko
ArtyomSavchenko merged commit b962dfb into hcengineering:develop Oct 13, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants