Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,12 @@ export function computeCpm(tasks, opts = {}) {
else if (l.type === 'SF') c = slf - l.lag + d;
else c = sls - l.lag; // FS
return Math.min(m, c);
}, Infinity)
// Seed with projectDurationDays (not Infinity): standard CPM caps every
// activity's Late Finish at project completion, then tightens it with
// successor constraints. With Infinity, a node whose only successor links
// are SS/FF/SF could take an LF looser than the project end, giving a
// truly-critical activity false total float (and an empty critical path).
}, projectDurationDays)
: projectDurationDays;
lf.set(id, finish);
ls.set(id, finish - d);
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"fuzz": "node --test tests/fuzz/*.mjs"
},
"dependencies": {
"@hono/node-server": "^1.19.14",
"@hono/node-server": "^2.0.12",
"hono": "^4.12.27"
},
"devDependencies": {
Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions tests/unit/dep-types.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,22 @@ const T = (id, duration, predecessors = '') => ({ id, duration, predecessors });
assert.equal(r.cycleDetected, true);
}

// backward pass caps Late Finish at the project duration. An SS long-pole with a
// short terminal follow-on: A (10d) alone drives the schedule end; B (2d) starts
// with A but finishes early. A must be critical with zero total float. Regression:
// seeding the successor reduce with Infinity left A's LF uncapped at 18 (> project
// duration 10), giving A a false slack of 8, marking it non-critical, and returning
// an EMPTY critical path for a schedule that plainly has one.
{
const r = computeCpm([T('A', 10), T('B', 2, 'ASS')]);
assert.equal(r.projectDurationDays, 10, 'A(0-10) is the long pole');
assert.equal(r.perTask.A.lf, 10, 'LF capped at project duration, not 18');
assert.equal(r.perTask.A.slack, 0, 'sole long-pole activity has zero total float');
assert.ok(r.perTask.A.critical, 'A is critical');
assert.deepEqual(r.criticalPath, ['A'], 'critical path is A, not empty');
// B keeps its real slack and stays non-critical.
assert.equal(r.perTask.B.slack, 8, 'B(0-2) has 8d float to the project end');
assert.equal(r.perTask.B.critical, false);
}

console.log('✓ dependency-type (SS/FF/SF+lag) tests passed');
Loading