gantt-lib 0.122.0 → 0.124.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -208,6 +208,159 @@ function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag
208
208
  return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
209
209
  }
210
210
 
211
+ // src/core/scheduling/criticalPath.ts
212
+ function computeCriticalPath(tasks, options = {}) {
213
+ const { businessDays = false, weekendPredicate } = options;
214
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
215
+ const childIds = /* @__PURE__ */ new Set();
216
+ for (const task of tasks) {
217
+ if (task.parentId) childIds.add(task.parentId);
218
+ }
219
+ const leafTasks = tasks.filter((task) => !childIds.has(task.id));
220
+ const leafIds = new Set(leafTasks.map((task) => task.id));
221
+ const leafIdsByAncestor = /* @__PURE__ */ new Map();
222
+ for (const leaf of leafTasks) {
223
+ let ancestor = leaf.parentId;
224
+ while (ancestor) {
225
+ const set = leafIdsByAncestor.get(ancestor) ?? /* @__PURE__ */ new Set();
226
+ set.add(leaf.id);
227
+ leafIdsByAncestor.set(ancestor, set);
228
+ ancestor = taskById.get(ancestor)?.parentId;
229
+ }
230
+ }
231
+ const durationOf = (task) => getTaskDuration(task.startDate, task.endDate, businessDays, weekendPredicate);
232
+ const projectStartDate = leafTasks.reduce((earliest, task) => {
233
+ const taskStart = parseDateOnly(task.startDate);
234
+ return taskStart.getTime() < earliest.getTime() ? taskStart : earliest;
235
+ }, parseDateOnly(leafTasks[0]?.startDate ?? /* @__PURE__ */ new Date()));
236
+ const offsetFromProjectStart = (date) => {
237
+ const targetDate = parseDateOnly(date);
238
+ if (businessDays && weekendPredicate) {
239
+ return getBusinessDayOffset(projectStartDate, targetDate, weekendPredicate);
240
+ }
241
+ return Math.round((targetDate.getTime() - projectStartDate.getTime()) / DAY_MS);
242
+ };
243
+ const scheduledStart = /* @__PURE__ */ new Map();
244
+ for (const leaf of leafTasks) {
245
+ scheduledStart.set(leaf.id, offsetFromProjectStart(leaf.startDate));
246
+ }
247
+ const succByPred = /* @__PURE__ */ new Map();
248
+ const predBySucc = /* @__PURE__ */ new Map();
249
+ const seenEdges = /* @__PURE__ */ new Set();
250
+ const addEdge = (predecessorId, succId, lag, type) => {
251
+ const edgeKey = `${predecessorId}|${succId}|${type}`;
252
+ if (seenEdges.has(edgeKey)) return;
253
+ seenEdges.add(edgeKey);
254
+ const outEdges = succByPred.get(predecessorId) ?? [];
255
+ outEdges.push({ succId, lag, type });
256
+ succByPred.set(predecessorId, outEdges);
257
+ const inEdges = predBySucc.get(succId) ?? [];
258
+ inEdges.push({ predId: predecessorId, lag, type });
259
+ predBySucc.set(succId, inEdges);
260
+ };
261
+ for (const succ of leafTasks) {
262
+ for (const dep of succ.dependencies ?? []) {
263
+ const lag = getDependencyLag(dep);
264
+ if (leafIds.has(dep.taskId)) {
265
+ addEdge(dep.taskId, succ.id, lag, dep.type);
266
+ continue;
267
+ }
268
+ const descendants = leafIdsByAncestor.get(dep.taskId);
269
+ if (!descendants || descendants.size === 0) continue;
270
+ for (const leafId of descendants) {
271
+ if (leafId === succ.id) continue;
272
+ addEdge(leafId, succ.id, lag, dep.type);
273
+ }
274
+ }
275
+ }
276
+ const indegree = /* @__PURE__ */ new Map();
277
+ for (const leaf of leafTasks) indegree.set(leaf.id, 0);
278
+ for (const edges of succByPred.values()) {
279
+ for (const { succId } of edges) {
280
+ indegree.set(succId, (indegree.get(succId) ?? 0) + 1);
281
+ }
282
+ }
283
+ const order = [];
284
+ const enqueued = /* @__PURE__ */ new Set();
285
+ const queue = leafTasks.filter((leaf) => (indegree.get(leaf.id) ?? 0) === 0).map((leaf) => leaf.id);
286
+ for (const id of queue) enqueued.add(id);
287
+ while (queue.length > 0) {
288
+ const current = queue.shift();
289
+ order.push(current);
290
+ for (const { succId } of succByPred.get(current) ?? []) {
291
+ const next = (indegree.get(succId) ?? 0) - 1;
292
+ indegree.set(succId, next);
293
+ if (next === 0 && !enqueued.has(succId)) {
294
+ enqueued.add(succId);
295
+ queue.push(succId);
296
+ }
297
+ }
298
+ }
299
+ const projectFinish = Math.max(
300
+ 0,
301
+ ...leafTasks.map((leaf) => (scheduledStart.get(leaf.id) ?? 0) + durationOf(leaf))
302
+ );
303
+ const LF = /* @__PURE__ */ new Map();
304
+ const LS = /* @__PURE__ */ new Map();
305
+ for (const leaf of leafTasks) LF.set(leaf.id, projectFinish);
306
+ for (let i = order.length - 1; i >= 0; i--) {
307
+ const id = order[i];
308
+ const task = taskById.get(id);
309
+ const durPred = durationOf(task);
310
+ const outEdges = succByPred.get(id) ?? [];
311
+ let lf = projectFinish;
312
+ if (outEdges.length > 0) {
313
+ const candidates = outEdges.map(({ succId, lag, type }) => {
314
+ const succLs = LS.get(succId) ?? projectFinish;
315
+ const durSucc = durationOf(taskById.get(succId));
316
+ switch (type) {
317
+ case "FS":
318
+ return succLs - lag;
319
+ case "SS":
320
+ return succLs - lag + durPred;
321
+ case "FF":
322
+ return succLs + durSucc - lag;
323
+ case "SF":
324
+ return succLs + durSucc - lag + durPred;
325
+ default:
326
+ return projectFinish;
327
+ }
328
+ });
329
+ lf = Math.min(projectFinish, ...candidates);
330
+ }
331
+ LF.set(id, lf);
332
+ LS.set(id, lf - durPred);
333
+ }
334
+ const critical = /* @__PURE__ */ new Set();
335
+ const ordered = new Set(order);
336
+ for (const leaf of leafTasks) {
337
+ if (!ordered.has(leaf.id)) continue;
338
+ const hasGraphEdge = (succByPred.get(leaf.id)?.length ?? 0) > 0 || (predBySucc.get(leaf.id)?.length ?? 0) > 0;
339
+ if (!hasGraphEdge && (leaf.dependencies?.length ?? 0) > 0) continue;
340
+ const es = scheduledStart.get(leaf.id) ?? 0;
341
+ const ls = LS.get(leaf.id) ?? 0;
342
+ if (ls - es <= 0) critical.add(leaf.id);
343
+ }
344
+ return critical;
345
+ }
346
+ function extendCriticalIdsToParents(criticalLeafIds, tasks) {
347
+ const result = new Set(criticalLeafIds);
348
+ const parentByChild = /* @__PURE__ */ new Map();
349
+ for (const task of tasks) {
350
+ if (task.parentId) parentByChild.set(task.id, task.parentId);
351
+ }
352
+ const stack = Array.from(criticalLeafIds);
353
+ while (stack.length > 0) {
354
+ const id = stack.pop();
355
+ const parentId = parentByChild.get(id);
356
+ if (parentId && !result.has(parentId)) {
357
+ result.add(parentId);
358
+ stack.push(parentId);
359
+ }
360
+ }
361
+ return result;
362
+ }
363
+
211
364
  // src/core/scheduling/commands.ts
212
365
  function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
213
366
  const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
@@ -1190,11 +1343,13 @@ export {
1190
1343
  calculateSuccessorDate,
1191
1344
  cascadeByLinks,
1192
1345
  clampTaskRangeForIncomingFS,
1346
+ computeCriticalPath,
1193
1347
  computeLagFromDates,
1194
1348
  computeParentDates,
1195
1349
  computeParentProgress,
1196
1350
  createCascadeContext,
1197
1351
  detectCycles,
1352
+ extendCriticalIdsToParents,
1198
1353
  findParentId,
1199
1354
  getAllDependencyEdges,
1200
1355
  getAllDescendants,