@outbuild-company/schedule-core 1.26.0 → 1.26.1

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.
package/dist/index.cjs CHANGED
@@ -1,16 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // src/activity-types/constants.ts
4
- var ACTIVITY_TYPE = {
5
- TASK: "task",
6
- PROJECT: "project",
7
- MILESTONE: "milestone"
8
- };
9
- var ACTIVITY_TYPES = Object.values(ACTIVITY_TYPE);
10
- function isActivityType(value) {
11
- return ACTIVITY_TYPES.includes(value);
12
- }
13
-
14
3
  // src/types.ts
15
4
  var NOTIFICATION_TYPE = {
16
5
  CONSTRAINT: "constraint"};
@@ -692,7 +681,10 @@ function createLinksApi(engine) {
692
681
  }
693
682
  function createScheduleNamespaces(engine) {
694
683
  const applyBaselinePoints2 = (points, options) => engine.dispatch(
695
- { kind: DISPATCH_ACTION_KIND.BASELINE_APPLY, points },
684
+ {
685
+ kind: DISPATCH_ACTION_KIND.BASELINE_APPLY,
686
+ points
687
+ },
696
688
  options
697
689
  );
698
690
  return {
@@ -1076,6 +1068,17 @@ function dispatchVisibilitySet(action, deps) {
1076
1068
  return { ok: true, changes };
1077
1069
  }
1078
1070
 
1071
+ // src/activity-types/constants.ts
1072
+ var ACTIVITY_TYPE = {
1073
+ TASK: "task",
1074
+ PROJECT: "project",
1075
+ MILESTONE: "milestone"
1076
+ };
1077
+ var ACTIVITY_TYPES = Object.values(ACTIVITY_TYPE);
1078
+ function isActivityType(value) {
1079
+ return ACTIVITY_TYPES.includes(value);
1080
+ }
1081
+
1079
1082
  // src/autoscheduler/constants.ts
1080
1083
  var TIMING = {
1081
1084
  YIELD_SLICE_MS: 8
@@ -2442,6 +2445,7 @@ var ACTIVITY_PROPERTY = {
2442
2445
  DURATION_HOURS: "durationHours",
2443
2446
  CONSTRAINT_TYPE: "constraintType",
2444
2447
  CONSTRAINT_DATE: "constraintDate",
2448
+ CALENDAR_ID: "calendarId",
2445
2449
  PROGRESS: "progress",
2446
2450
  COST: "cost",
2447
2451
  USED_COST: "usedCost",
@@ -2572,6 +2576,17 @@ function filterTopoOrder(order, scopedIds) {
2572
2576
  return filtered;
2573
2577
  }
2574
2578
 
2579
+ // src/constants/progress.ts
2580
+ var NOT_STARTED_PROGRESS = 0;
2581
+ var COMPLETED_PROGRESS = 100;
2582
+ var MAX_TOLERATED_PROGRESS = COMPLETED_PROGRESS + Number.EPSILON * COMPLETED_PROGRESS * 2;
2583
+ function isCompletedProgress(progress) {
2584
+ return progress >= COMPLETED_PROGRESS && progress <= MAX_TOLERATED_PROGRESS;
2585
+ }
2586
+ function isAutoSchedulingEnabledForProgress(progress) {
2587
+ return progress === NOT_STARTED_PROGRESS;
2588
+ }
2589
+
2575
2590
  // src/autoscheduler/utils/parent-link-expansion.ts
2576
2591
  function isSummary2(id, adapter) {
2577
2592
  const activity = adapter.getActivity(id);
@@ -2751,12 +2766,8 @@ function deriveSummaryBoundsWithDates(summaryId, adapter, resolveDates) {
2751
2766
  if (earliestStart === null || latestEnd === null) return null;
2752
2767
  return { startDate: earliestStart, endDate: latestEnd };
2753
2768
  }
2754
- function deriveSummaryBounds(summaryId, adapter) {
2755
- return deriveSummaryBoundsWithDates(summaryId, adapter, (leafId) => {
2756
- const leaf = adapter.getActivity(leafId);
2757
- if (!leaf) return null;
2758
- return { startDate: leaf.startDate, endDate: leaf.endDate };
2759
- });
2769
+ function deriveSummaryBounds(summaryId, adapter, resolveDates = (leafId) => adapter.getActivity(leafId)) {
2770
+ return deriveSummaryBoundsWithDates(summaryId, adapter, resolveDates);
2760
2771
  }
2761
2772
  function endpointSpanHours(endpoint, bounds, adapter) {
2762
2773
  if (!endpoint) return 0;
@@ -2782,31 +2793,48 @@ function hasIncomingRawLink(leafId, linksByActivity) {
2782
2793
  const bucket = linksByActivity.get(leafKey) ?? [];
2783
2794
  return bucket.some((rawLink) => String(rawLink.target) === leafKey);
2784
2795
  }
2785
- function targetOffsetForLeaf(leafId, targetStart, source, adapter, linksByActivity) {
2786
- if (hasIncomingRawLink(leafId, linksByActivity)) return 0;
2796
+ function targetOffsetForLeaf(leafId, targetStart, adapter, linksByActivity, targetEndpointIsFinish) {
2797
+ if (!targetEndpointIsFinish && hasIncomingRawLink(leafId, linksByActivity))
2798
+ return 0;
2787
2799
  const leaf = adapter.getActivity(leafId);
2788
- if (!targetStart || !leaf || !source) return 0;
2800
+ if (!targetStart || !leaf) return 0;
2789
2801
  return adapter.calculateDuration({
2790
2802
  startDate: targetStart,
2791
2803
  endDate: leaf.startDate,
2792
- task: source
2804
+ task: leaf
2793
2805
  });
2794
2806
  }
2795
2807
  function resolveExpansionAnchors(originalLink, adapter) {
2796
2808
  const source = adapter.getActivity(originalLink.source);
2797
2809
  const target = adapter.getActivity(originalLink.target);
2798
2810
  const sourceBounds = deriveSummaryBounds(originalLink.source, adapter);
2799
- const targetBounds = deriveSummaryBounds(originalLink.target, adapter);
2811
+ const fullTargetBounds = deriveSummaryBounds(originalLink.target, adapter);
2812
+ const targetBounds = deriveSummaryBoundsWithDates(
2813
+ originalLink.target,
2814
+ adapter,
2815
+ (leafId) => {
2816
+ const leaf = adapter.getActivity(leafId);
2817
+ if (!leaf) return null;
2818
+ const hasFixedDate = leaf.constraintDate !== null && (leaf.constraintType === CONSTRAINT_TYPE.MSO || leaf.constraintType === CONSTRAINT_TYPE.MFO);
2819
+ const canSchedule = leaf.autoScheduling && leaf.progress !== COMPLETED_PROGRESS && !hasFixedDate;
2820
+ if (!canSchedule) return null;
2821
+ return { startDate: leaf.startDate, endDate: leaf.endDate };
2822
+ }
2823
+ );
2800
2824
  const sourceEndpointIsStart = originalLink.type === LINK_TYPE.START_TO_START || originalLink.type === LINK_TYPE.START_TO_FINISH;
2801
2825
  const targetEndpointIsFinish = originalLink.type === LINK_TYPE.FINISH_TO_FINISH || originalLink.type === LINK_TYPE.START_TO_FINISH;
2802
2826
  return {
2803
2827
  source,
2804
2828
  sourceBounds,
2805
2829
  targetStart: targetBounds?.startDate ?? target?.startDate,
2830
+ targetBounds,
2831
+ hasFrozenTargetStart: Boolean(
2832
+ fullTargetBounds && targetBounds && fullTargetBounds.startDate < targetBounds.startDate
2833
+ ),
2834
+ targetEndpointIsFinish,
2806
2835
  userLag: originalLink.lag ?? 0,
2807
2836
  sourceEndpointIsStart,
2808
- targetLag: targetEndpointIsFinish ? -endpointSpanHours(target, targetBounds, adapter) : 0,
2809
- summarySourceId: originalLink.type === LINK_TYPE.START_TO_START && sourceBounds ? originalLink.source : void 0,
2837
+ summarySourceId: sourceEndpointIsStart && sourceBounds ? originalLink.source : void 0,
2810
2838
  finishAnchoredSummaryTargetId: targetEndpointIsFinish && targetBounds ? originalLink.target : void 0
2811
2839
  };
2812
2840
  }
@@ -2815,7 +2843,9 @@ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, a
2815
2843
  const { source, sourceBounds, targetStart, userLag } = anchors;
2816
2844
  const {
2817
2845
  sourceEndpointIsStart,
2818
- targetLag,
2846
+ targetBounds,
2847
+ targetEndpointIsFinish,
2848
+ hasFrozenTargetStart,
2819
2849
  summarySourceId,
2820
2850
  finishAnchoredSummaryTargetId
2821
2851
  } = anchors;
@@ -2824,12 +2854,13 @@ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, a
2824
2854
  const sourceLag = sourceEndpointIsStart ? sourceLagForLeaf(srcId, source, sourceBounds, adapter) : 0;
2825
2855
  for (const tgtId of leafIds) {
2826
2856
  if (String(srcId) === String(tgtId)) continue;
2857
+ const targetLag = targetEndpointIsFinish ? -endpointSpanHours(adapter.getActivity(tgtId), targetBounds, adapter) : 0;
2827
2858
  const offTarget = targetOffsetForLeaf(
2828
2859
  tgtId,
2829
2860
  targetStart,
2830
- source,
2831
2861
  adapter,
2832
- linksByActivity
2862
+ linksByActivity,
2863
+ targetEndpointIsFinish
2833
2864
  );
2834
2865
  expanded.push(
2835
2866
  buildVirtualLink(
@@ -2841,7 +2872,8 @@ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, a
2841
2872
  targetLag,
2842
2873
  userLag + offTarget,
2843
2874
  summarySourceId,
2844
- finishAnchoredSummaryTargetId ?? (offTarget !== 0 && targetLag === 0 ? originalLink.target : void 0)
2875
+ finishAnchoredSummaryTargetId ?? ((offTarget !== 0 || hasFrozenTargetStart) && targetLag === 0 ? originalLink.target : void 0),
2876
+ targetBounds ? originalLink.target : void 0
2845
2877
  )
2846
2878
  );
2847
2879
  counter += 1;
@@ -2849,7 +2881,7 @@ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, a
2849
2881
  }
2850
2882
  return counter;
2851
2883
  }
2852
- function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag, targetLag, trueLag, summarySourceId, offsetSummaryTargetId) {
2884
+ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag, targetLag, trueLag, summarySourceId, offsetSummaryTargetId, lagActivityId) {
2853
2885
  return {
2854
2886
  id: `virtual_${originalLink.id}_${counter}`,
2855
2887
  source: sourceId,
@@ -2858,6 +2890,7 @@ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag,
2858
2890
  lag: originalLink.lag,
2859
2891
  freeSlackHours: null,
2860
2892
  isCritical: false,
2893
+ ...lagActivityId === void 0 ? {} : { _lagActivityId: lagActivityId },
2861
2894
  _sourceLag: sourceLag,
2862
2895
  _targetLag: targetLag,
2863
2896
  _trueLag: trueLag,
@@ -2928,17 +2961,6 @@ function findConnectedGroupForTrigger(triggerId, links) {
2928
2961
  return { activityIds, links: groupLinks };
2929
2962
  }
2930
2963
 
2931
- // src/constants/progress.ts
2932
- var NOT_STARTED_PROGRESS = 0;
2933
- var COMPLETED_PROGRESS = 100;
2934
- var MAX_TOLERATED_PROGRESS = COMPLETED_PROGRESS + Number.EPSILON * COMPLETED_PROGRESS * 2;
2935
- function isCompletedProgress(progress) {
2936
- return progress >= COMPLETED_PROGRESS && progress <= MAX_TOLERATED_PROGRESS;
2937
- }
2938
- function isAutoSchedulingEnabledForProgress(progress) {
2939
- return progress === NOT_STARTED_PROGRESS;
2940
- }
2941
-
2942
2964
  // src/autoscheduler/engine/link-calculator.ts
2943
2965
  var MILLISECONDS_PER_HOUR = 36e5;
2944
2966
  var INSTANT_MILLISECONDS = 1;
@@ -3016,11 +3038,15 @@ function normalize(predecessor, successor, link) {
3016
3038
  };
3017
3039
  }
3018
3040
  }
3019
- function summaryAnchoredSourceLag(link, predecessor, adapter) {
3041
+ function summaryAnchoredSourceLag(link, predecessor, adapter, resolveDates) {
3020
3042
  if (link._summarySourceId === void 0) return null;
3021
3043
  const summarySource = adapter.getActivity(link._summarySourceId);
3022
3044
  if (!summarySource) return null;
3023
- const summaryBounds = deriveSummaryBounds(link._summarySourceId, adapter);
3045
+ const summaryBounds = deriveSummaryBounds(
3046
+ link._summarySourceId,
3047
+ adapter,
3048
+ resolveDates
3049
+ );
3024
3050
  if (!summaryBounds) return null;
3025
3051
  return -adapter.calculateDuration({
3026
3052
  startDate: summaryBounds.startDate,
@@ -3065,7 +3091,7 @@ function reverseRelationshipLagAnchor(successor, relationshipLagTask, adapter) {
3065
3091
  task: relationshipLagTask
3066
3092
  });
3067
3093
  }
3068
- function calculateSuccessorStartFromLink(predecessor, successor, link, adapter, trueLagTask = successor) {
3094
+ function calculateSuccessorStartFromLink(predecessor, successor, link, adapter, trueLagTask = successor, resolveDates) {
3069
3095
  const {
3070
3096
  sourceLag: frozenSourceLag,
3071
3097
  targetLag,
@@ -3073,11 +3099,12 @@ function calculateSuccessorStartFromLink(predecessor, successor, link, adapter,
3073
3099
  isFS,
3074
3100
  isFF
3075
3101
  } = normalize(predecessor, successor, link);
3076
- const sourceLag = summaryAnchoredSourceLag(link, predecessor, adapter) ?? frozenSourceLag;
3102
+ const sourceLag = summaryAnchoredSourceLag(link, predecessor, adapter, resolveDates) ?? frozenSourceLag;
3103
+ const lagActivity = link._lagActivityId === void 0 ? null : adapter.getActivity(link._lagActivityId);
3077
3104
  const totalLag = sourceLag + targetLag + trueLag;
3078
3105
  const isZeroDurationSuccessor = successor.durationHours === 0;
3079
3106
  const hasLag = isZeroDurationSuccessor ? totalLag !== 0 : sourceLag !== 0 || targetLag !== 0 || trueLag !== 0;
3080
- if (!hasLag) {
3107
+ if (!hasLag && (!lagActivity || isZeroDurationSuccessor && sourceLag === 0 && targetLag === 0 && trueLag === 0)) {
3081
3108
  return noLagSuccessorStart(
3082
3109
  predecessor,
3083
3110
  successor,
@@ -3086,7 +3113,7 @@ function calculateSuccessorStartFromLink(predecessor, successor, link, adapter,
3086
3113
  isFS || isFF
3087
3114
  );
3088
3115
  }
3089
- const baseTask = isFS ? successor : predecessor;
3116
+ const baseTask = isFS ? lagActivity ?? successor : predecessor;
3090
3117
  let date2 = relationshipLagAnchor(predecessor, baseTask, trueLagTask, adapter);
3091
3118
  if (sourceLag !== 0) {
3092
3119
  date2 = adapter.calculateEndDate({
@@ -3095,15 +3122,30 @@ function calculateSuccessorStartFromLink(predecessor, successor, link, adapter,
3095
3122
  task: predecessor
3096
3123
  });
3097
3124
  }
3098
- if (targetLag !== 0) {
3099
- date2 = adapter.calculateEndDate({
3100
- startDate: date2,
3101
- durationHours: targetLag,
3102
- task: successor
3125
+ if (lagActivity) {
3126
+ date2 = addTrueLag(date2, link.lag ?? 0, lagActivity, adapter);
3127
+ date2 = adapter.getClosestWorkTime({
3128
+ date: date2,
3129
+ dir: isFS || link.type === LINK_TYPE.START_TO_START ? "future" : "past",
3130
+ task: lagActivity
3103
3131
  });
3104
- }
3105
- if (trueLag !== 0) {
3106
- date2 = addTrueLag(date2, trueLag, trueLagTask, adapter);
3132
+ const leafOffset = targetLag + trueLag - (link.lag ?? 0);
3133
+ if (leafOffset !== 0) {
3134
+ date2 = adapter.calculateEndDate({
3135
+ startDate: date2,
3136
+ durationHours: leafOffset,
3137
+ task: successor
3138
+ });
3139
+ }
3140
+ } else {
3141
+ if (targetLag !== 0) {
3142
+ date2 = adapter.calculateEndDate({
3143
+ startDate: date2,
3144
+ durationHours: targetLag,
3145
+ task: successor
3146
+ });
3147
+ }
3148
+ if (trueLag !== 0) date2 = addTrueLag(date2, trueLag, trueLagTask, adapter);
3107
3149
  }
3108
3150
  if (successor.durationHours === 0) {
3109
3151
  if (isFS || isFF) {
@@ -3143,16 +3185,25 @@ function calculatePredecessorEndFromLink(successor, predecessor, link, adapter,
3143
3185
  successor,
3144
3186
  link
3145
3187
  );
3146
- let date2 = reverseRelationshipLagAnchor(successor, trueLagTask, adapter);
3147
- if (trueLag !== 0) {
3148
- date2 = addTrueLag(date2, -trueLag, trueLagTask, adapter);
3149
- }
3150
- if (targetLag !== 0) {
3151
- date2 = adapter.calculateEndDate({
3152
- startDate: date2,
3153
- durationHours: -targetLag,
3154
- task: successor
3155
- });
3188
+ const lagActivity = link._lagActivityId === void 0 ? null : adapter.getActivity(link._lagActivityId);
3189
+ let date2 = lagActivity ? new Date(successor.startDate) : reverseRelationshipLagAnchor(successor, trueLagTask, adapter);
3190
+ if (lagActivity) {
3191
+ const leafOffset = targetLag + trueLag - (link.lag ?? 0);
3192
+ if (leafOffset !== 0)
3193
+ date2 = adapter.calculateEndDate({
3194
+ startDate: date2,
3195
+ durationHours: -leafOffset,
3196
+ task: successor
3197
+ });
3198
+ date2 = addTrueLag(date2, -(link.lag ?? 0), lagActivity, adapter);
3199
+ } else {
3200
+ if (trueLag !== 0) date2 = addTrueLag(date2, -trueLag, trueLagTask, adapter);
3201
+ if (targetLag !== 0)
3202
+ date2 = adapter.calculateEndDate({
3203
+ startDate: date2,
3204
+ durationHours: -targetLag,
3205
+ task: successor
3206
+ });
3156
3207
  }
3157
3208
  if (sourceLag !== 0) {
3158
3209
  date2 = adapter.calculateEndDate({
@@ -3254,8 +3305,71 @@ function shouldSkipLagForMultiDrag(predecessorId, flags, draggedIds) {
3254
3305
  return flags.dragWithMultiple && isPredecessorBeingDragged(predecessorId, draggedIds);
3255
3306
  }
3256
3307
 
3308
+ // src/autoscheduler/engine/summary-finish-groups.ts
3309
+ function collectSummaryFinishGroups(links, adapter, resolveDates) {
3310
+ const groups = /* @__PURE__ */ new Map();
3311
+ for (const link of links) {
3312
+ if (link.type !== LINK_TYPE.FINISH_TO_FINISH && link.type !== LINK_TYPE.START_TO_FINISH)
3313
+ continue;
3314
+ const summaryId = link._summaryTargetId;
3315
+ if (summaryId === void 0) continue;
3316
+ if (groups.has(summaryId)) continue;
3317
+ let independentlyDriven = false;
3318
+ const starts = /* @__PURE__ */ new Map();
3319
+ let movableEnd = null;
3320
+ let movableLeafId = null;
3321
+ const bounds = deriveSummaryBounds(summaryId, adapter, (id) => {
3322
+ const leaf = adapter.getActivity(id);
3323
+ if (!leaf) return null;
3324
+ const dates = resolveDates(id);
3325
+ starts.set(id, dates.startDate);
3326
+ if (!leaf.autoScheduling || leaf.progress === COMPLETED_PROGRESS || leaf.constraintType !== CONSTRAINT_TYPE.ASAP)
3327
+ independentlyDriven = true;
3328
+ for (const linkId of adapter.getIncomingLinkIds(id)) {
3329
+ const incoming = adapter.getLink(linkId);
3330
+ if (incoming && String(incoming.source) !== String(summaryId) && !adapter.isChildOf(incoming.source, summaryId))
3331
+ independentlyDriven = true;
3332
+ }
3333
+ const hasFixedDate = leaf.constraintDate !== null && (leaf.constraintType === CONSTRAINT_TYPE.MSO || leaf.constraintType === CONSTRAINT_TYPE.MFO);
3334
+ const movable = leaf.autoScheduling && leaf.progress !== COMPLETED_PROGRESS && !hasFixedDate;
3335
+ if (movable && (movableEnd === null || dates.endDate > movableEnd || dates.endDate.getTime() === movableEnd.getTime() && id < (movableLeafId ?? id))) {
3336
+ movableEnd = dates.endDate;
3337
+ movableLeafId = id;
3338
+ }
3339
+ return dates;
3340
+ });
3341
+ const drivingLeafIds = /* @__PURE__ */ new Set();
3342
+ const pending = movableLeafId === null ? [] : [movableLeafId];
3343
+ while (pending.length > 0) {
3344
+ const id = pending.pop();
3345
+ if (drivingLeafIds.has(id)) continue;
3346
+ drivingLeafIds.add(id);
3347
+ for (const linkId of [
3348
+ ...adapter.getIncomingLinkIds(id),
3349
+ ...adapter.getOutgoingLinkIds(id)
3350
+ ]) {
3351
+ const link2 = adapter.getLink(linkId);
3352
+ if (!link2) continue;
3353
+ const neighbor = String(link2.source) === String(id) ? link2.target : link2.source;
3354
+ if (starts.has(neighbor) && !drivingLeafIds.has(neighbor))
3355
+ pending.push(neighbor);
3356
+ }
3357
+ }
3358
+ groups.set(
3359
+ summaryId,
3360
+ independentlyDriven && bounds ? {
3361
+ endDate: bounds.endDate,
3362
+ movableEnd,
3363
+ starts,
3364
+ drivingLeafIds
3365
+ } : null
3366
+ );
3367
+ }
3368
+ return groups;
3369
+ }
3370
+
3257
3371
  // src/autoscheduler/engine/asap-pass.ts
3258
- async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
3372
+ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans, deferSummaryFinishes = false) {
3259
3373
  const incomingLinks = /* @__PURE__ */ new Map();
3260
3374
  for (const link of links) {
3261
3375
  const key = String(link.target);
@@ -3271,6 +3385,11 @@ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
3271
3385
  if (snap) return { startDate: snap.startDate, endDate: snap.endDate };
3272
3386
  return { startDate: /* @__PURE__ */ new Date(), endDate: /* @__PURE__ */ new Date() };
3273
3387
  };
3388
+ const summaryFinishes = collectSummaryFinishGroups(
3389
+ links,
3390
+ adapter,
3391
+ getActivityDates
3392
+ );
3274
3393
  const flags = adapter.getFlags();
3275
3394
  const draggedIds = options.draggedIds ?? [];
3276
3395
  let sliceStart = performance.now();
@@ -3307,21 +3426,60 @@ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
3307
3426
  startDate: multiDragDates.startDate,
3308
3427
  endDate: multiDragDates.endDate
3309
3428
  };
3310
- const effectiveLink = shouldSkipLagForMultiDrag(
3429
+ let effectiveLink = shouldSkipLagForMultiDrag(
3311
3430
  link.source,
3312
3431
  flags,
3313
3432
  draggedIds
3314
3433
  ) ? { ...link, lag: 0 } : link;
3434
+ const summaryFinish = effectiveLink._summaryTargetId === void 0 || effectiveLink.type !== LINK_TYPE.FINISH_TO_FINISH && effectiveLink.type !== LINK_TYPE.START_TO_FINISH ? null : summaryFinishes.get(effectiveLink._summaryTargetId);
3435
+ if (summaryFinish) {
3436
+ if (deferSummaryFinishes) continue;
3437
+ const requiredFinish = calculateSuccessorStartFromLink(
3438
+ adjustedPred,
3439
+ { ...activity, durationHours: 0 },
3440
+ { ...effectiveLink, _targetLag: 0, _trueLag: effectiveLink.lag },
3441
+ adapter,
3442
+ activity,
3443
+ getActivityDates
3444
+ ).successorStart;
3445
+ const lagActivity = adapter.getActivity(effectiveLink._summaryTargetId) ?? activity;
3446
+ const satisfied = summaryFinish.endDate >= requiredFinish || adapter.calculateDuration({
3447
+ startDate: summaryFinish.endDate,
3448
+ endDate: requiredFinish,
3449
+ task: lagActivity
3450
+ }) === 0;
3451
+ if (satisfied || summaryFinish.movableEnd === null || !summaryFinish.drivingLeafIds.has(activityId))
3452
+ continue;
3453
+ effectiveLink = {
3454
+ ...effectiveLink,
3455
+ _targetLag: -adapter.calculateDuration({
3456
+ startDate: summaryFinish.starts.get(activityId) ?? activity.startDate,
3457
+ endDate: summaryFinish.movableEnd,
3458
+ task: activity
3459
+ }),
3460
+ _trueLag: effectiveLink.lag
3461
+ };
3462
+ }
3463
+ let summarySourceMovedThisRun = false;
3315
3464
  const result = calculateSuccessorStartFromLink(
3316
3465
  adjustedPred,
3317
3466
  activity,
3318
3467
  effectiveLink,
3319
- adapter
3468
+ adapter,
3469
+ activity,
3470
+ (id) => {
3471
+ const dates = getActivityDates(id);
3472
+ const snapshot = adapter.getActivity(id);
3473
+ if (snapshot && (dates.startDate.getTime() !== snapshot.startDate.getTime() || dates.endDate.getTime() !== snapshot.endDate.getTime())) {
3474
+ summarySourceMovedThisRun = true;
3475
+ }
3476
+ return dates;
3477
+ }
3320
3478
  );
3321
3479
  if (!maxStart || result.successorStart > maxStart) {
3322
3480
  maxStart = result.successorStart;
3323
3481
  drivingLinkId = String(link.id);
3324
- drivingOffsetIsStale = link._summaryTargetId !== void 0 && !predecessorMovedThisRun && (link._targetLag === 0 || options.preventSummaryTargetPullback === true);
3482
+ drivingOffsetIsStale = link._summaryTargetId !== void 0 && !predecessorMovedThisRun && !summarySourceMovedThisRun && (link._targetLag === 0 || options.preventSummaryTargetPullback === true);
3325
3483
  }
3326
3484
  }
3327
3485
  const staleOffsetWouldPullBack = maxStart !== null && drivingOffsetIsStale && maxStart < activity.startDate;
@@ -4552,14 +4710,26 @@ var AutoScheduler = class {
4552
4710
  return { ...emptyResult, cancelled: true };
4553
4711
  }
4554
4712
  const phaseAsap = startPhase("schedule.asapPass");
4555
- const asapPlans = await asapPass(
4713
+ const hasSummaryFinishLinks = scopedLinks.some(
4714
+ (link) => link._summaryTargetId !== void 0 && (link.type === LINK_TYPE.FINISH_TO_FINISH || link.type === LINK_TYPE.START_TO_FINISH)
4715
+ );
4716
+ const initialAsapPlans = await asapPass(
4556
4717
  topoResult.ordered,
4557
4718
  scopedLinks,
4558
4719
  this.adapter,
4559
4720
  options,
4560
4721
  isCurrent,
4561
- plans
4722
+ plans,
4723
+ hasSummaryFinishLinks
4562
4724
  );
4725
+ const asapPlans = hasSummaryFinishLinks ? await asapPass(
4726
+ topoResult.ordered,
4727
+ scopedLinks,
4728
+ this.adapter,
4729
+ options,
4730
+ isCurrent,
4731
+ initialAsapPlans
4732
+ ) : initialAsapPlans;
4563
4733
  endPhase(phaseAsap);
4564
4734
  if ((await yieldBetweenPhases(isCurrent)).aborted) {
4565
4735
  return { ...emptyResult, cancelled: true };
@@ -4721,14 +4891,16 @@ function roundProgressPerLevel(value) {
4721
4891
  }
4722
4892
 
4723
4893
  // src/propagations/upward/progress-rollup.ts
4724
- function recomputeAllProgressRollup(adapter) {
4894
+ function recomputeAllProgressRollup(adapter, criterion) {
4895
+ const zeroCostBasis = /* @__PURE__ */ new Map();
4725
4896
  for (const parentId of parentsDeepestFirst(adapter)) {
4726
- rollupParent(parentId, adapter);
4897
+ rollupParent(parentId, adapter, criterion, zeroCostBasis);
4727
4898
  }
4728
4899
  }
4729
- function recomputeProgressRollupForParents(parentIdsDeepestFirst, adapter) {
4900
+ function recomputeProgressRollupForParents(parentIdsDeepestFirst, adapter, criterion) {
4901
+ const zeroCostBasis = /* @__PURE__ */ new Map();
4730
4902
  for (const parentId of parentIdsDeepestFirst) {
4731
- rollupParent(parentId, adapter);
4903
+ rollupParent(parentId, adapter, criterion, zeroCostBasis);
4732
4904
  }
4733
4905
  }
4734
4906
  function parentsDeepestFirst(adapter) {
@@ -4752,7 +4924,7 @@ function depthOf(id, adapter) {
4752
4924
  }
4753
4925
  return depth;
4754
4926
  }
4755
- function rollupParent(parentId, adapter) {
4927
+ function rollupParent(parentId, adapter, criterion, zeroCostBasis) {
4756
4928
  const childIds = adapter.getChildren(parentId);
4757
4929
  if (childIds.length === 0) return;
4758
4930
  const children = [];
@@ -4764,12 +4936,20 @@ function rollupParent(parentId, adapter) {
4764
4936
  ponderator: Number(child.ponderator ?? 0)
4765
4937
  });
4766
4938
  }
4767
- applyWeightedProgressRollup(parentId, children, adapter);
4939
+ applyWeightedProgressRollup(
4940
+ parentId,
4941
+ children,
4942
+ adapter,
4943
+ criterion,
4944
+ zeroCostBasis
4945
+ );
4768
4946
  }
4769
- function applyWeightedProgressRollup(parentId, children, adapter) {
4947
+ function applyWeightedProgressRollup(parentId, children, adapter, criterion, zeroCostBasis = /* @__PURE__ */ new Map()) {
4770
4948
  const parent = adapter.getActivity(parentId);
4771
4949
  if (!parent) return;
4772
- const rollup = computeWeightedProgressRollup(children);
4950
+ const weightedProgress = computeWeightedProgressRollup(children);
4951
+ const hasZeroCostBasis = weightedProgress === null && criterion === ACTIVITY_PONDERATOR_CRITERION.COST && hasOnlyZeroBaselineCosts(parentId, adapter, zeroCostBasis) === true;
4952
+ const rollup = hasZeroCostBasis ? 0 : weightedProgress;
4773
4953
  if (rollup === null) {
4774
4954
  const fallback = adapter.getProgressFallback(parentId);
4775
4955
  if (fallback !== null) {
@@ -4786,6 +4966,43 @@ function applyWeightedProgressRollup(parentId, children, adapter) {
4786
4966
  roundProgressPerLevel(rollup)
4787
4967
  );
4788
4968
  }
4969
+ function hasOnlyZeroBaselineCosts(parentId, adapter, zeroCostBasis) {
4970
+ const pending = [...adapter.getChildren(parentId)];
4971
+ let hasBaseline = false;
4972
+ while (pending.length > 0) {
4973
+ const activityId = pending.pop();
4974
+ const cachedBasis = zeroCostBasis.get(activityId);
4975
+ if (cachedBasis === false) {
4976
+ zeroCostBasis.set(parentId, false);
4977
+ return false;
4978
+ }
4979
+ if (cachedBasis !== void 0) {
4980
+ hasBaseline ||= cachedBasis === true;
4981
+ continue;
4982
+ }
4983
+ const activity = adapter.getActivity(activityId);
4984
+ if (!activity) continue;
4985
+ const children = adapter.getChildren(activityId);
4986
+ if (children.length > 0) {
4987
+ pending.push(...children);
4988
+ continue;
4989
+ }
4990
+ const basis2 = zeroBaselineCost(activity);
4991
+ hasBaseline ||= basis2 !== null;
4992
+ if (basis2 === false) {
4993
+ zeroCostBasis.set(parentId, false);
4994
+ return false;
4995
+ }
4996
+ }
4997
+ const basis = hasBaseline ? true : null;
4998
+ zeroCostBasis.set(parentId, basis);
4999
+ return basis;
5000
+ }
5001
+ function zeroBaselineCost(activity) {
5002
+ const baseline = getActiveBaseline(activity);
5003
+ if (!baseline) return null;
5004
+ return activity.type === ACTIVITY_TYPE.MILESTONE || baseline.cost === 0;
5005
+ }
4789
5006
  function setExplicitSummaryProgressFallbacks(activityId, progress, adapter) {
4790
5007
  const pending = [activityId];
4791
5008
  while (pending.length > 0) {
@@ -4812,7 +5029,8 @@ async function updateParentBoundsFromChildren(adapter, dirtyIds, recomputeProgre
4812
5029
  const parents = dirtyIds ? findAncestorsOfDirty(adapter, dirtyIds) : findAllParents(adapter);
4813
5030
  const progressMode = recomputeProgress ? options.progressMode ?? PARENT_PROGRESS_MODE.FREEZE_WITHOUT_CONTRIBUTION : PARENT_PROGRESS_MODE.SKIP;
4814
5031
  await recomputeParentsCooperatively(parents, adapter, progressMode, {
4815
- recomputeRealWork: options.recomputeRealWork ?? false
5032
+ recomputeRealWork: options.recomputeRealWork ?? false,
5033
+ criterion: options.criterion
4816
5034
  });
4817
5035
  }
4818
5036
  function recomputeSingleParentDisplayBounds(parentId, adapter) {
@@ -4821,6 +5039,7 @@ function recomputeSingleParentDisplayBounds(parentId, adapter) {
4821
5039
  async function recomputeParentsCooperatively(parents, adapter, progressMode, options = {
4822
5040
  recomputeRealWork: false
4823
5041
  }) {
5042
+ const zeroCostBasis = /* @__PURE__ */ new Map();
4824
5043
  const YIELD_EVERY_N_PARENTS = 200;
4825
5044
  let processed = 0;
4826
5045
  for (const parentId of parents) {
@@ -4828,7 +5047,9 @@ async function recomputeParentsCooperatively(parents, adapter, progressMode, opt
4828
5047
  parentId,
4829
5048
  adapter,
4830
5049
  progressMode,
4831
- options.recomputeRealWork
5050
+ options.recomputeRealWork,
5051
+ options.criterion,
5052
+ zeroCostBasis
4832
5053
  );
4833
5054
  processed += 1;
4834
5055
  if (processed % YIELD_EVERY_N_PARENTS === 0) {
@@ -4902,7 +5123,7 @@ function aggregateChildren(childrenIds, adapter, includeRealWork) {
4902
5123
  }
4903
5124
  return { minStart, maxEnd, realWorkSum, weightedChildren };
4904
5125
  }
4905
- function recomputeParentFromChildren(parentId, adapter, progressMode = PARENT_PROGRESS_MODE.FREEZE_WITHOUT_CONTRIBUTION, recomputeRealWork = false) {
5126
+ function recomputeParentFromChildren(parentId, adapter, progressMode = PARENT_PROGRESS_MODE.FREEZE_WITHOUT_CONTRIBUTION, recomputeRealWork = false, criterion, zeroCostBasis = /* @__PURE__ */ new Map()) {
4906
5127
  const parent = adapter.getActivity(parentId);
4907
5128
  if (!parent) return;
4908
5129
  const childrenIds = adapter.getChildren(parentId);
@@ -4918,7 +5139,13 @@ function recomputeParentFromChildren(parentId, adapter, progressMode = PARENT_PR
4918
5139
  adapter.setActivityField(parentId, "endDate", newEnd);
4919
5140
  adapter.setActivityField(parentId, "durationHours", durationHours);
4920
5141
  if (progressMode !== PARENT_PROGRESS_MODE.SKIP) {
4921
- applyWeightedProgressRollup(parentId, aggregate.weightedChildren, adapter);
5142
+ applyWeightedProgressRollup(
5143
+ parentId,
5144
+ aggregate.weightedChildren,
5145
+ adapter,
5146
+ criterion,
5147
+ zeroCostBasis
5148
+ );
4922
5149
  }
4923
5150
  if (recomputeRealWork) {
4924
5151
  adapter.setActivityField(parentId, "realWorkHours", aggregate.realWorkSum);
@@ -5250,9 +5477,13 @@ var POST_PROCESSOR = {
5250
5477
  };
5251
5478
 
5252
5479
  // src/columns/shared/error-codes.ts
5480
+ var PARSE_ERROR = {
5481
+ EMPTY_CALENDAR: "empty_calendar"};
5253
5482
  var VALIDATION_REASON = {
5254
5483
  UNCHANGED: "unchanged",
5255
- NEGATIVE_DURATION: "negative_duration"};
5484
+ NEGATIVE_DURATION: "negative_duration",
5485
+ UNKNOWN_CALENDAR: "unknown_calendar"
5486
+ };
5256
5487
  var EDIT_DENIAL_REASON = {
5257
5488
  COMPLETED: "completed",
5258
5489
  SUMMARY: "summary",
@@ -5545,7 +5776,7 @@ var costPipeline = {
5545
5776
  parseInput(rawValue) {
5546
5777
  if (String(rawValue).trim() === "") return parsed(0);
5547
5778
  const numericValue = parseFloat(String(rawValue));
5548
- if (Number.isNaN(numericValue)) return parseError("not_a_number");
5779
+ if (!Number.isFinite(numericValue)) return parseError("not_a_number");
5549
5780
  return parsed(numericValue);
5550
5781
  },
5551
5782
  validate() {
@@ -5595,7 +5826,7 @@ var usedCostPipeline = {
5595
5826
  parseInput(rawValue) {
5596
5827
  if (String(rawValue).trim() === "") return parsed(0);
5597
5828
  const numericValue = parseFloat(String(rawValue));
5598
- if (Number.isNaN(numericValue)) return parseError("not_a_number");
5829
+ if (!Number.isFinite(numericValue)) return parseError("not_a_number");
5599
5830
  return parsed(numericValue);
5600
5831
  },
5601
5832
  validate() {
@@ -5648,7 +5879,7 @@ var hhWorkTimePipeline = {
5648
5879
  parseInput(rawValue) {
5649
5880
  if (String(rawValue).trim() === "") return parsed(0, rawValue);
5650
5881
  const numericValue = parseFloat(String(rawValue));
5651
- if (Number.isNaN(numericValue)) return parseError("not_a_number");
5882
+ if (!Number.isFinite(numericValue)) return parseError("not_a_number");
5652
5883
  return parsed(numericValue);
5653
5884
  },
5654
5885
  validate() {
@@ -6046,27 +6277,33 @@ function isUnconstrained(activity) {
6046
6277
 
6047
6278
  // src/columns/calendar-id/calendar-id-pipeline.ts
6048
6279
  var calendarIdPipeline = {
6049
- targetField: "calendarId",
6280
+ targetField: ACTIVITY_PROPERTY.CALENDAR_ID,
6050
6281
  canEdit() {
6051
6282
  return canEdit();
6052
6283
  },
6053
6284
  parseInput(rawValue) {
6285
+ if (rawValue === null) return parsed(null);
6054
6286
  const value = String(rawValue).trim();
6055
- if (isEmpty(value)) return parseError("empty_calendar");
6287
+ if (isEmpty(value)) return parseError(PARSE_ERROR.EMPTY_CALENDAR);
6056
6288
  return parsed(value);
6057
6289
  },
6058
6290
  validate(activity, _oldValue, newValue, ctx) {
6059
- if (isUnchangedCalendar(activity.calendarId, newValue))
6060
- return invalid("unchanged");
6061
- if (!ctx.calendars.getCalendar(newValue))
6062
- return invalid("unknown_calendar");
6291
+ const calendarId = newValue === null ? ctx.getDefaultCalendarId() : newValue;
6292
+ if (isUnchangedCalendar(activity.calendarId, calendarId) && (newValue !== null || activity.assignedDefaultCalendar))
6293
+ return invalid(VALIDATION_REASON.UNCHANGED);
6294
+ if (calendarId === null || !ctx.hasCalendar(calendarId))
6295
+ return invalid(VALIDATION_REASON.UNKNOWN_CALENDAR);
6063
6296
  return valid();
6064
6297
  },
6065
6298
  transform(activity, newValue, ctx) {
6066
- return fieldChanges(buildCalendarPatch(activity, newValue, ctx), {
6067
- autoSchedule: true,
6068
- postProcessors: []
6069
- });
6299
+ const calendarId = newValue === null ? ctx.getDefaultCalendarId() : newValue;
6300
+ return fieldChanges(
6301
+ buildCalendarPatch(activity, calendarId, newValue === null, ctx),
6302
+ {
6303
+ autoSchedule: true,
6304
+ postProcessors: []
6305
+ }
6306
+ );
6070
6307
  },
6071
6308
  trackEvent: COLUMN_TRACK_EVENT.CALENDAR_ASSIGNMENT
6072
6309
  };
@@ -6076,12 +6313,12 @@ function isEmpty(value) {
6076
6313
  function isUnchangedCalendar(currentId, newId) {
6077
6314
  return currentId === newId;
6078
6315
  }
6079
- function buildCalendarPatch(activity, newCalendarId, ctx) {
6316
+ function buildCalendarPatch(activity, newCalendarId, assignedDefaultCalendar, ctx) {
6080
6317
  const fields = {
6081
6318
  calendarId: newCalendarId,
6082
- assignedDefaultCalendar: false
6319
+ assignedDefaultCalendar
6083
6320
  };
6084
- const newCalendar = ctx.calendars.getCalendar(newCalendarId);
6321
+ const newCalendar = newCalendarId === null ? null : ctx.calendars.getCalendar(newCalendarId);
6085
6322
  if (newCalendar) {
6086
6323
  const startIsWorking = newCalendar.isWorkTime(activity.startDate, "hour");
6087
6324
  const calendarStart = newCalendar.getClosestWorkTime({
@@ -6291,6 +6528,7 @@ var P6_ACTIVITY_TYPE = {
6291
6528
  TASK: "task",
6292
6529
  LEVEL_OF_EFFORT: "level_of_effort",
6293
6530
  START_MILESTONE: "start_milestone",
6531
+ WBS_SUMMARY: "wbs_summary",
6294
6532
  RESOURCE_DEPENDENT: "resource_dependent"
6295
6533
  };
6296
6534
  var P6_EXECUTION_STATUS = {
@@ -6456,9 +6694,10 @@ var actualStartPipeline = {
6456
6694
  transform(activity, newValue, ctx) {
6457
6695
  if (newValue === null) return actualStartChanges(activity, null);
6458
6696
  const calendar = resolveActivityCalendar(activity, ctx);
6697
+ const firstAssignment = activity.p6?.actualStart == null;
6459
6698
  return actualStartChanges(
6460
6699
  activity,
6461
- defaultActualStartTime(newValue, calendar)
6700
+ firstAssignment ? defaultActualStartTime(newValue, calendar) : new Date(newValue)
6462
6701
  );
6463
6702
  }
6464
6703
  };
@@ -6467,9 +6706,10 @@ var actualFinishPipeline = {
6467
6706
  transform(activity, newValue, ctx) {
6468
6707
  if (newValue === null) return actualFinishChanges(activity, null);
6469
6708
  const calendar = resolveActivityCalendar(activity, ctx);
6709
+ const firstAssignment = activity.p6?.actualFinish == null;
6470
6710
  return actualFinishChanges(
6471
6711
  activity,
6472
- defaultActualFinishTime(newValue, calendar)
6712
+ firstAssignment ? defaultActualFinishTime(newValue, calendar) : new Date(newValue)
6473
6713
  );
6474
6714
  }
6475
6715
  };
@@ -12412,7 +12652,7 @@ function collectInsertedIds(captured) {
12412
12652
  }
12413
12653
 
12414
12654
  // src/propagations/downward/recompute-ponderator-for-parent.ts
12415
- function recomputePonderatorsForParent(parentId, criterion, adapter) {
12655
+ function recomputePonderatorsForParent(parentId, criterion, adapter, basisById = /* @__PURE__ */ new Map()) {
12416
12656
  if (!parentId || parentId === ROOT_PARENT_ID) return;
12417
12657
  const childIds = adapter.getChildren(parentId);
12418
12658
  if (childIds.length === 0) return;
@@ -12420,15 +12660,21 @@ function recomputePonderatorsForParent(parentId, criterion, adapter) {
12420
12660
  const baselineScopedIds = /* @__PURE__ */ new Set();
12421
12661
  let denominator = 0;
12422
12662
  for (const childId of childIds) {
12423
- const peso = criterionValue(String(childId), criterion, adapter);
12424
- pesoById.set(String(childId), peso);
12425
- denominator += peso;
12426
- if (hasActiveBaselineInBranch(String(childId), adapter)) {
12663
+ const basis = baselineWeightingBasis(
12664
+ String(childId),
12665
+ criterion,
12666
+ adapter,
12667
+ basisById
12668
+ );
12669
+ pesoById.set(String(childId), basis.magnitude);
12670
+ denominator += basis.magnitude;
12671
+ if (basis.hasActiveBaseline) {
12427
12672
  baselineScopedIds.add(String(childId));
12428
12673
  }
12429
12674
  }
12430
12675
  const relationValue = denominator ? 100 / denominator : 0;
12431
- const zeroMagnitudeShare = baselineScopedIds.size ? 100 / baselineScopedIds.size : 0;
12676
+ const hasEqualShare = criterion !== ACTIVITY_PONDERATOR_CRITERION.COST && baselineScopedIds.size > 0;
12677
+ const zeroMagnitudeShare = hasEqualShare ? 100 / baselineScopedIds.size : 0;
12432
12678
  for (const childId of childIds) {
12433
12679
  const child = adapter.getActivity(childId);
12434
12680
  if (!child) continue;
@@ -12453,39 +12699,39 @@ function assignedPonderator(childId, child, pesoById, baselineScopedIds, denomin
12453
12699
  }
12454
12700
  return !Array.isArray(child.baselinePoints) || child.baselinePoints.length === 0 ? 0 : null;
12455
12701
  }
12456
- function hasActiveBaselineInBranch(activityId, adapter) {
12457
- const activity = adapter.getActivity(activityId);
12458
- if (!activity) return false;
12459
- if (getActiveBaseline(activity)) return true;
12460
- if (activity.type !== ACTIVITY_TYPE.PROJECT) return false;
12461
- return adapter.getChildren(activityId).some((childId) => hasActiveBaselineInBranch(String(childId), adapter));
12462
- }
12463
- function criterionValue(childId, criterion, adapter) {
12464
- const child = adapter.getActivity(childId);
12465
- if (!child) return 0;
12466
- if (child.type === ACTIVITY_TYPE.PROJECT) {
12467
- return sumRecursiveBaselineMagnitude(childId, criterion, adapter);
12468
- }
12469
- const active = getActiveBaseline(child);
12470
- if (!active) return 0;
12471
- if (child.type === ACTIVITY_TYPE.MILESTONE) return 0;
12472
- return baselineMagnitude(active, criterion);
12473
- }
12474
- function sumRecursiveBaselineMagnitude(activityId, criterion, adapter) {
12475
- let sum2 = 0;
12476
- for (const childId of adapter.getChildren(activityId)) {
12477
- const child = adapter.getActivity(childId);
12478
- if (!child) continue;
12479
- if (child.type === ACTIVITY_TYPE.PROJECT) {
12480
- sum2 += sumRecursiveBaselineMagnitude(String(childId), criterion, adapter);
12702
+ function baselineWeightingBasis(activityId, criterion, adapter, basisById) {
12703
+ const pending = [
12704
+ { id: activityId, expanded: false }
12705
+ ];
12706
+ while (pending.length > 0) {
12707
+ const { id, expanded } = pending.pop();
12708
+ if (basisById.has(id)) continue;
12709
+ const activity = adapter.getActivity(id);
12710
+ const baseline = getActiveBaseline(activity);
12711
+ if (activity?.type !== ACTIVITY_TYPE.PROJECT) {
12712
+ basisById.set(id, {
12713
+ magnitude: baseline && activity?.type !== ACTIVITY_TYPE.MILESTONE ? baselineMagnitude(baseline, criterion) : 0,
12714
+ hasActiveBaseline: baseline !== null
12715
+ });
12716
+ continue;
12717
+ }
12718
+ const children = adapter.getChildren(id);
12719
+ if (!expanded) {
12720
+ pending.push({ id, expanded: true });
12721
+ children.forEach((childId) => {
12722
+ pending.push({ id: String(childId), expanded: false });
12723
+ });
12481
12724
  continue;
12482
12725
  }
12483
- const active = getActiveBaseline(child);
12484
- if (active && child.type !== ACTIVITY_TYPE.MILESTONE) {
12485
- sum2 += baselineMagnitude(active, criterion);
12726
+ const basis = { magnitude: 0, hasActiveBaseline: baseline !== null };
12727
+ for (const childId of children) {
12728
+ const child = basisById.get(String(childId));
12729
+ basis.magnitude += child.magnitude;
12730
+ basis.hasActiveBaseline ||= child.hasActiveBaseline;
12486
12731
  }
12732
+ basisById.set(id, basis);
12487
12733
  }
12488
- return sum2;
12734
+ return basisById.get(activityId);
12489
12735
  }
12490
12736
  function baselineMagnitude(baseline, criterion) {
12491
12737
  if (criterion === ACTIVITY_PONDERATOR_CRITERION.COST) {
@@ -12501,10 +12747,72 @@ function toNumber(v) {
12501
12747
  return typeof n === "number" && Number.isFinite(n) ? n : 0;
12502
12748
  }
12503
12749
 
12750
+ // src/dispatch/shared/structural-ponderators.ts
12751
+ function recomputeStructuralPonderators(adapter, criterion) {
12752
+ const journal = adapter.peekWriteCapture();
12753
+ if (!journal) return;
12754
+ const parents = /* @__PURE__ */ new Map();
12755
+ const removedParents = /* @__PURE__ */ new Map();
12756
+ for (const operation of journal.ops) {
12757
+ if (operation.kind === WRITE_OPERATION_KIND.REMOVE) {
12758
+ removedParents.set(operation.activityId, operation.before.parentId);
12759
+ }
12760
+ }
12761
+ const collectParents = (parentId, lostBaseline) => {
12762
+ let current = parentId;
12763
+ while (current !== null && !isRootParent(current)) {
12764
+ const existing = parents.get(current);
12765
+ if (existing === true || existing === lostBaseline) break;
12766
+ parents.set(current, lostBaseline);
12767
+ current = removedParents.has(current) ? removedParents.get(current) ?? null : adapter.getParentId(current);
12768
+ }
12769
+ };
12770
+ for (const [activityId, before] of journal.before) {
12771
+ const after = adapter.getActivity(activityId);
12772
+ const parentChanged = after && before.parentId !== after.parentId;
12773
+ if (!parentChanged || !after) continue;
12774
+ const hasBaseline = getActiveBaseline(before) !== null || hasBaselineInBranch(activityId, adapter);
12775
+ collectParents(before.parentId, hasBaseline);
12776
+ collectParents(after.parentId, false);
12777
+ }
12778
+ for (const operation of journal.ops) {
12779
+ if (operation.kind === WRITE_OPERATION_KIND.REMOVE) {
12780
+ if (getActiveBaseline(operation.before))
12781
+ collectParents(operation.before.parentId, true);
12782
+ } else if (operation.kind === WRITE_OPERATION_KIND.INSERT) {
12783
+ const inserted = adapter.getActivity(operation.activityId);
12784
+ if (inserted && hasBaselineInBranch(inserted.id, adapter))
12785
+ collectParents(inserted.parentId, false);
12786
+ }
12787
+ }
12788
+ for (const [parentId, lostBaseline] of parents) {
12789
+ if (lostBaseline || hasBaselineInBranch(parentId, adapter)) {
12790
+ recomputePonderatorsForParent(parentId, criterion, adapter);
12791
+ }
12792
+ }
12793
+ }
12794
+ function hasBaselineInBranch(activityId, adapter) {
12795
+ const pending = [activityId];
12796
+ while (pending.length > 0) {
12797
+ const current = pending.pop();
12798
+ if (current === void 0) continue;
12799
+ const activity = adapter.getActivity(current);
12800
+ if (activity && getActiveBaseline(activity)) return true;
12801
+ pending.push(...adapter.getChildren(current));
12802
+ }
12803
+ return false;
12804
+ }
12805
+
12504
12806
  // src/dispatch/shared/post-mutation.ts
12505
12807
  var AUTOSCHEDULE_SCOPE = { ROOTS: "roots" };
12506
12808
  async function runPostMutation(deps, args) {
12507
- recomputePromotedSummaryPonderators(deps);
12809
+ const isConversion = args.action.kind === DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_CONVERT;
12810
+ if (!isConversion) {
12811
+ recomputeStructuralPonderators(
12812
+ deps.adapter,
12813
+ deps.sector.activityCreter ?? ACTIVITY_PONDERATOR_CRITERION.DURATION
12814
+ );
12815
+ }
12508
12816
  const progressInvalidation = resolveProgressInvalidation(deps.adapter);
12509
12817
  const effectiveArgs = resolveTransactionAutoscheduleArgs(deps.adapter, args);
12510
12818
  const passes = resolvePasses(
@@ -12525,7 +12833,8 @@ async function runPostMutation(deps, args) {
12525
12833
  effectiveArgs,
12526
12834
  schedule.scheduledIds,
12527
12835
  passes,
12528
- progressInvalidation
12836
+ progressInvalidation,
12837
+ deps.sector.activityCreter ?? ACTIVITY_PONDERATOR_CRITERION.DURATION
12529
12838
  );
12530
12839
  await runDerivedRecomputes(deps, passes, args.now);
12531
12840
  synchronizeInactiveDataDateDrafts(deps.adapter);
@@ -12574,37 +12883,6 @@ function collectLinkMutationTargets(adapter) {
12574
12883
  }
12575
12884
  return targets;
12576
12885
  }
12577
- function recomputePromotedSummaryPonderators(deps) {
12578
- const journal = deps.adapter.peekWriteCapture();
12579
- if (!journal) return;
12580
- const visited = /* @__PURE__ */ new Set();
12581
- const criterion = deps.sector.activityCreter ?? ACTIVITY_PONDERATOR_CRITERION.DURATION;
12582
- for (const [activityId, before] of journal.before) {
12583
- const after = deps.adapter.getActivity(activityId);
12584
- if (before.type === ACTIVITY_TYPE.PROJECT || after?.type !== ACTIVITY_TYPE.PROJECT || getActiveBaseline(after) || !hasActiveBaselineDescendant(activityId, deps.adapter)) {
12585
- continue;
12586
- }
12587
- let current = activityId;
12588
- while (current !== null && !isRootParent(current)) {
12589
- if (!visited.has(current)) {
12590
- recomputePonderatorsForParent(current, criterion, deps.adapter);
12591
- visited.add(current);
12592
- }
12593
- current = deps.adapter.getParentId(current);
12594
- }
12595
- }
12596
- }
12597
- function hasActiveBaselineDescendant(activityId, adapter) {
12598
- const pending = [...adapter.getChildren(activityId)];
12599
- while (pending.length > 0) {
12600
- const childId = pending.pop();
12601
- if (childId === void 0) continue;
12602
- const child = adapter.getActivity(childId);
12603
- if (child && getActiveBaseline(child)) return true;
12604
- pending.push(...adapter.getChildren(childId));
12605
- }
12606
- return false;
12607
- }
12608
12886
  async function dispatchScheduleAutoSchedule(action, options, deps) {
12609
12887
  if (action.activityId !== void 0 && !deps.adapter.activityExists(action.activityId)) {
12610
12888
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
@@ -12757,7 +13035,7 @@ async function refreshMutatedParentBounds(adapter, args) {
12757
13035
  if (mutated.size === 0) return;
12758
13036
  await updateParentBoundsFromChildren(adapter, mutated, false);
12759
13037
  }
12760
- async function rollUpParentBounds(adapter, args, scheduledIds, passes, progressInvalidation) {
13038
+ async function rollUpParentBounds(adapter, args, scheduledIds, passes, progressInvalidation, criterion) {
12761
13039
  if (passes.has(DERIVED_PASS.PARENT_PROGRESS)) {
12762
13040
  restoreDemotedProgressFallbacks(
12763
13041
  adapter,
@@ -12774,6 +13052,7 @@ async function rollUpParentBounds(adapter, args, scheduledIds, passes, progressI
12774
13052
  passes.has(DERIVED_PASS.PARENT_PROGRESS),
12775
13053
  {
12776
13054
  recomputeRealWork: passes.has(DERIVED_PASS.REAL_WORK),
13055
+ criterion,
12777
13056
  progressMode: "freeze-when-no-contribution"
12778
13057
  }
12779
13058
  );
@@ -13023,6 +13302,8 @@ function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
13023
13302
  after: Number(afterLink.lag)
13024
13303
  };
13025
13304
  }
13305
+ const unchangedLink = beforeLink !== void 0 && Object.keys(fields).length === 0;
13306
+ if (unchangedLink) continue;
13026
13307
  out.push({
13027
13308
  id: String(op.linkId),
13028
13309
  kind: ENTITY_CHANGE_KIND.UPDATED,
@@ -16126,6 +16407,69 @@ function hasInvalidPersistedProgress(action) {
16126
16407
  return progress !== void 0 && (action.activityIds.length !== 1 || !Number.isFinite(progress) || progress < 0 || progress > 100);
16127
16408
  }
16128
16409
 
16410
+ // src/dispatch/shared/lookahead-conversion-progress.ts
16411
+ function buildConversionProgressSnapshots(activities, criterion) {
16412
+ const snapshots = new Map(
16413
+ activities.map((activity) => [
16414
+ activity.id,
16415
+ structuredCloneActivity(activity)
16416
+ ])
16417
+ );
16418
+ const childrenByParent = /* @__PURE__ */ new Map();
16419
+ for (const activity of activities) {
16420
+ if (activity.parentId === null) continue;
16421
+ const siblings = childrenByParent.get(activity.parentId) ?? [];
16422
+ siblings.push(activity);
16423
+ childrenByParent.set(activity.parentId, siblings);
16424
+ }
16425
+ const magnitudes = /* @__PURE__ */ new Map();
16426
+ for (const activity of [...activities].reverse()) {
16427
+ const children = childrenByParent.get(activity.id) ?? [];
16428
+ const isSummary3 = children.length > 0;
16429
+ const magnitude = isSummary3 ? children.reduce(
16430
+ (sum2, child) => sum2 + requiredMagnitude(magnitudes, child.id),
16431
+ 0
16432
+ ) : liveMagnitude(activity, criterion);
16433
+ magnitudes.set(activity.id, magnitude);
16434
+ if (!isSummary3) continue;
16435
+ const hasMagnitude = magnitude > 0;
16436
+ const permitsEqualShare = criterion !== ACTIVITY_PONDERATOR_CRITERION.COST;
16437
+ for (const child of children) {
16438
+ const snapshot2 = requiredSnapshot(snapshots, child.id);
16439
+ const ponderator = hasMagnitude ? 100 * requiredMagnitude(magnitudes, child.id) / magnitude : permitsEqualShare ? 100 / children.length : 0;
16440
+ snapshots.set(child.id, { ...snapshot2, ponderator });
16441
+ }
16442
+ const weightedChildren = children.map(
16443
+ (child) => requiredSnapshot(snapshots, child.id)
16444
+ );
16445
+ const progress = computeWeightedProgressRollup(weightedChildren);
16446
+ const snapshot = requiredSnapshot(snapshots, activity.id);
16447
+ snapshots.set(activity.id, {
16448
+ ...snapshot,
16449
+ progress: roundProgressPerLevel(progress ?? 0)
16450
+ });
16451
+ }
16452
+ return snapshots;
16453
+ }
16454
+ function liveMagnitude(activity, criterion) {
16455
+ if (activity.type === ACTIVITY_TYPE.PROJECT) return 0;
16456
+ if (criterion === ACTIVITY_PONDERATOR_CRITERION.COST) return activity.cost;
16457
+ if (criterion === ACTIVITY_PONDERATOR_CRITERION.WORK_HOURS)
16458
+ return activity.workHours;
16459
+ return activity.durationHours;
16460
+ }
16461
+ function requiredMagnitude(magnitudes, activityId) {
16462
+ const magnitude = magnitudes.get(activityId);
16463
+ if (magnitude === void 0)
16464
+ throw new Error(`Missing conversion magnitude ${activityId}`);
16465
+ return magnitude;
16466
+ }
16467
+ function requiredSnapshot(snapshots, activityId) {
16468
+ const snapshot = snapshots.get(activityId);
16469
+ if (!snapshot) throw new Error(`Missing conversion snapshot ${activityId}`);
16470
+ return snapshot;
16471
+ }
16472
+
16129
16473
  // src/dispatch/handlers/activity-lookahead-convert.ts
16130
16474
  function buildLookaheadConversionPlan(input, deps) {
16131
16475
  const { adapter } = deps;
@@ -16140,7 +16484,11 @@ function buildLookaheadConversionPlan(input, deps) {
16140
16484
  (activityId) => requested.has(activityId) && !hasAncestorInSet(activityId, requested, adapter)
16141
16485
  );
16142
16486
  const branches = conversionRootIds.map(
16143
- (rootId) => planConversionBranch(rootId, adapter)
16487
+ (rootId) => planConversionBranch(
16488
+ rootId,
16489
+ adapter,
16490
+ input.criterion ?? deps.sector.activityCreter ?? ACTIVITY_PONDERATOR_CRITERION.DURATION
16491
+ )
16144
16492
  );
16145
16493
  const conversionGroups = branches.map(
16146
16494
  ({ conversionRootId, convertedActivities }) => ({
@@ -16215,7 +16563,7 @@ async function dispatchActivityLookaheadConvert(action, options, deps) {
16215
16563
  });
16216
16564
  return buildConversionResult(plan, context, changes);
16217
16565
  }
16218
- function planConversionBranch(rootId, adapter) {
16566
+ function planConversionBranch(rootId, adapter, criterion) {
16219
16567
  const root = requiredActivity(rootId, adapter);
16220
16568
  const branch = collectDescendantIds([rootId], adapter);
16221
16569
  branch.delete(rootId);
@@ -16232,15 +16580,19 @@ function planConversionBranch(rootId, adapter) {
16232
16580
  (activity) => activity.type === ACTIVITY_TYPE.MILESTONE
16233
16581
  );
16234
16582
  const persistedIds = collectPersistedIds(root, convertible);
16583
+ const progressSnapshots = buildConversionProgressSnapshots(
16584
+ [root, ...convertible],
16585
+ criterion
16586
+ );
16587
+ const rootSnapshot = progressSnapshots.get(root.id);
16588
+ if (!rootSnapshot) throw new Error(`Missing conversion snapshot ${root.id}`);
16235
16589
  return {
16236
16590
  conversionRootId: rootId,
16237
- convertedActivities: convertible.map(
16238
- (activity) => buildConvertedSnapshot(activity, persistedIds)
16239
- ),
16591
+ convertedActivities: [...progressSnapshots.values()].filter((activity) => activity.id !== rootId).map((activity) => buildConvertedSnapshot(activity, persistedIds)),
16240
16592
  milestoneMoves: buildMilestoneMoves(root, milestones),
16241
16593
  activityIdsToDelete: convertible.map((activity) => activity.id),
16242
16594
  formerParent: {
16243
- activity: structuredCloneActivity(root),
16595
+ activity: rootSnapshot,
16244
16596
  hasConvertedActivities: convertible.length > 0
16245
16597
  }
16246
16598
  };
@@ -16324,10 +16676,12 @@ function applyFormerParentState(plan, deletedIds, adapter) {
16324
16676
  idsRemoved: deletedIds
16325
16677
  });
16326
16678
  adapter.setActivityFields(activity.id, {
16679
+ progress: activity.progress,
16327
16680
  isLookahead: activity.isLookahead || hasConvertedActivities,
16328
16681
  hasLookaheadTasks: activity.hasLookaheadTasks || hasConvertedActivities,
16329
16682
  ...cleanup?.fields ?? {}
16330
16683
  });
16684
+ adapter.setProgressFallback(activity.id, null);
16331
16685
  }
16332
16686
  }
16333
16687
  function recomputeConversionRollups(plan, adapter) {
@@ -17406,8 +17760,9 @@ function isDateColumn(column) {
17406
17760
  return column === COLUMN.START_DATE || column === COLUMN.END_DATE;
17407
17761
  }
17408
17762
  async function dispatchDatesBatch(action, options, deps) {
17409
- if (action.edits.some((edit) => !isDateColumn(edit.column))) {
17410
- return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
17763
+ const rejection = validateBatchEdits(action, deps);
17764
+ if (rejection !== null) {
17765
+ return { ok: false, reason: rejection };
17411
17766
  }
17412
17767
  const { adapter, scheduler, sector } = deps;
17413
17768
  const verdicts = [];
@@ -17483,6 +17838,17 @@ async function dispatchDatesBatch(action, options, deps) {
17483
17838
  })
17484
17839
  };
17485
17840
  }
17841
+ function validateBatchEdits(action, deps) {
17842
+ const hasInvalidColumn = action.edits.some(
17843
+ (edit) => !isDateColumn(edit.column)
17844
+ );
17845
+ if (hasInvalidColumn) return REJECTION_REASON.INVALID_VALUE;
17846
+ const hasMissingActivity = action.edits.some(
17847
+ (edit) => deps.ctx.activityReader.getActivity(edit.activityId) === null
17848
+ );
17849
+ if (hasMissingActivity) return REJECTION_REASON.ACTIVITY_NOT_FOUND;
17850
+ return null;
17851
+ }
17486
17852
  function applyOneEdit(edit, deps, options) {
17487
17853
  const { ctx } = deps;
17488
17854
  const activityId = edit.activityId;
@@ -18222,12 +18588,13 @@ function applyBaselineOverlay(state, points, now, reporter, defaultBaseCalendarI
18222
18588
 
18223
18589
  // src/propagations/downward/recompute-all-ponderators.ts
18224
18590
  function recomputeAllPonderators(criterion, adapter) {
18591
+ const basisById = /* @__PURE__ */ new Map();
18225
18592
  adapter.forEachActivity((_a, id) => {
18226
18593
  adapter.setActivityField(id, COLUMN.PONDERATOR, 0);
18227
18594
  });
18228
18595
  adapter.forEachActivity((_a, id) => {
18229
18596
  if (adapter.getChildren(id).length > 0) {
18230
- recomputePonderatorsForParent(String(id), criterion, adapter);
18597
+ recomputePonderatorsForParent(String(id), criterion, adapter, basisById);
18231
18598
  }
18232
18599
  });
18233
18600
  }
@@ -18235,7 +18602,7 @@ function recomputeAllPonderators(criterion, adapter) {
18235
18602
  // src/dispatch/shared/baseline-weighted-fields.ts
18236
18603
  function recomputeBaselineWeightedFields(deps, criterion) {
18237
18604
  recomputeAllPonderators(criterion, deps.adapter);
18238
- recomputeAllProgressRollup(deps.adapter);
18605
+ recomputeAllProgressRollup(deps.adapter, criterion);
18239
18606
  if (deps.now) {
18240
18607
  runExpectedProgressBase(
18241
18608
  deps.adapter,
@@ -18279,7 +18646,8 @@ async function dispatchBaselineApply(action, deps) {
18279
18646
  action.points,
18280
18647
  deps.now,
18281
18648
  deps.reporter,
18282
- deps.defaultBaseCalendarId ?? null
18649
+ deps.defaultBaseCalendarId ?? null,
18650
+ void 0
18283
18651
  );
18284
18652
  applyBaselineVariancePass(deps.adapter, deps.sector.hoursPerDay);
18285
18653
  if (activeBaselineSignature(deps) !== activeBaselineBefore) {
@@ -19010,7 +19378,10 @@ function dispatchCellCopy(action, deps) {
19010
19378
  if (!pipeline) {
19011
19379
  return { ok: false, reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN };
19012
19380
  }
19013
- const canonicalValue = readPipelineTargetValue(activity, pipeline.targetField);
19381
+ const canonicalValue = readPipelineTargetValue(
19382
+ activity,
19383
+ pipeline.targetField
19384
+ );
19014
19385
  if (action.column === COLUMN.DURATION && !Number.isFinite(canonicalValue)) {
19015
19386
  return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
19016
19387
  }
@@ -19336,6 +19707,7 @@ function coerceActivityFieldValue(field, value) {
19336
19707
  }
19337
19708
 
19338
19709
  // src/calendar/adapter.ts
19710
+ var WORK_CALENDAR_UNIT = { MINUTE: "minute" };
19339
19711
  function createWorkCalendarAdapter(instance, options = {}) {
19340
19712
  const defaultUnit = options.defaultUnit ?? CALENDAR_UNIT.HOUR;
19341
19713
  return {
@@ -19356,11 +19728,18 @@ function wrapWorkCalendar(calendar, defaultUnit) {
19356
19728
  });
19357
19729
  },
19358
19730
  calculateEndDate(start, durationHours) {
19359
- return calendar.calculateEndDate({
19360
- start_date: start,
19731
+ const fractionalHours = defaultUnit === CALENDAR_UNIT.HOUR && !Number.isInteger(durationHours);
19732
+ const sourceRemainder = start.getUTCSeconds() * 1e3 + start.getUTCMilliseconds();
19733
+ const shouldPreserveSourcePrecision = fractionalHours && sourceRemainder !== 0 && calendar.isWorkTime({ date: start, unit: WORK_CALENDAR_UNIT.MINUTE });
19734
+ const calculationStart = shouldPreserveSourcePrecision ? new Date(start.getTime() - sourceRemainder) : start;
19735
+ const end = fractionalHours ? calculateFractionalEndDate(calendar, calculationStart, durationHours) : calendar.calculateEndDate({
19736
+ start_date: calculationStart,
19361
19737
  duration: durationHours,
19362
19738
  unit: defaultUnit
19363
19739
  });
19740
+ if (!shouldPreserveSourcePrecision) return end;
19741
+ const workingEnd = nextWorkingMinute(calendar, end);
19742
+ return new Date(workingEnd.getTime() + sourceRemainder);
19364
19743
  },
19365
19744
  calculateDuration(start, end) {
19366
19745
  return calendar.calculateDuration({
@@ -19377,6 +19756,91 @@ function wrapWorkCalendar(calendar, defaultUnit) {
19377
19756
  }
19378
19757
  };
19379
19758
  }
19759
+ function calculateFractionalEndDate(calendar, start, durationHours) {
19760
+ const roundedMinutes = Math.round(Math.abs(durationHours) * 60);
19761
+ const shouldUseWorkingPrefixes = roundedMinutes > 0 && calendar.isWorkTime({ date: start, unit: WORK_CALENDAR_UNIT.MINUTE });
19762
+ if (!shouldUseWorkingPrefixes)
19763
+ return calendar.calculateEndDate({
19764
+ start_date: start,
19765
+ duration: durationHours * 60,
19766
+ unit: WORK_CALENDAR_UNIT.MINUTE
19767
+ });
19768
+ let end = start;
19769
+ let remainingMinutes = roundedMinutes;
19770
+ const direction = durationHours < 0 ? -1 : 1;
19771
+ while (remainingMinutes > 0) {
19772
+ const reference = direction < 0 ? new Date(end.getTime() - 1) : end;
19773
+ if (!calendar.isWorkTime({ date: reference, unit: WORK_CALENDAR_UNIT.MINUTE })) {
19774
+ end = direction < 0 ? calendar.getClosestWorkTime({
19775
+ date: reference,
19776
+ dir: WORK_TIME_DIRECTION.PAST,
19777
+ unit: WORK_CALENDAR_UNIT.MINUTE
19778
+ }) : nextWorkingMinute(calendar, end);
19779
+ }
19780
+ const prefix = calculateWorkingMinutePrefix(
19781
+ calendar,
19782
+ end,
19783
+ Math.min(remainingMinutes, 60),
19784
+ direction
19785
+ );
19786
+ end = prefix.end;
19787
+ remainingMinutes -= prefix.minutes;
19788
+ }
19789
+ return end;
19790
+ }
19791
+ function calculateWorkingMinutePrefix(calendar, start, maximumMinutes, direction) {
19792
+ const fullPrefix = calendar.calculateEndDate({
19793
+ start_date: start,
19794
+ duration: direction * maximumMinutes,
19795
+ unit: WORK_CALENDAR_UNIT.MINUTE
19796
+ });
19797
+ const isFullPrefixContiguous = fullPrefix.getTime() === start.getTime() + direction * maximumMinutes * 6e4;
19798
+ if (isFullPrefixContiguous)
19799
+ return { end: fullPrefix, minutes: maximumMinutes };
19800
+ let minutes = 0;
19801
+ let limit = maximumMinutes - 1;
19802
+ let end = start;
19803
+ while (minutes < limit) {
19804
+ const testedMinutes = Math.ceil((minutes + limit) / 2);
19805
+ const candidate = calendar.calculateEndDate({
19806
+ start_date: start,
19807
+ duration: direction * testedMinutes,
19808
+ unit: WORK_CALENDAR_UNIT.MINUTE
19809
+ });
19810
+ const isContiguousWorkingTime = candidate.getTime() === start.getTime() + direction * testedMinutes * 6e4;
19811
+ if (isContiguousWorkingTime) {
19812
+ minutes = testedMinutes;
19813
+ end = candidate;
19814
+ } else {
19815
+ limit = testedMinutes - 1;
19816
+ }
19817
+ }
19818
+ if (minutes === 0)
19819
+ throw new Error("Calendar cannot calculate a working-minute prefix");
19820
+ return { end, minutes };
19821
+ }
19822
+ function nextWorkingMinute(calendar, date2) {
19823
+ if (calendar.isWorkTime({ date: date2, unit: WORK_CALENDAR_UNIT.MINUTE }))
19824
+ return date2;
19825
+ const nextWorkingDay2 = calendar.calculateEndDate({
19826
+ start_date: date2,
19827
+ duration: 1,
19828
+ unit: CALENDAR_UNIT.DAY
19829
+ });
19830
+ const nextDayStart = new Date(nextWorkingDay2);
19831
+ nextDayStart.setUTCHours(0, 0, 0, 0);
19832
+ const nextShiftStart = calendar.getClosestWorkTime({
19833
+ date: nextDayStart,
19834
+ dir: WORK_TIME_DIRECTION.FUTURE,
19835
+ unit: WORK_CALENDAR_UNIT.MINUTE
19836
+ });
19837
+ const closest = calendar.getClosestWorkTime({
19838
+ date: date2,
19839
+ dir: WORK_TIME_DIRECTION.FUTURE,
19840
+ unit: WORK_CALENDAR_UNIT.MINUTE
19841
+ });
19842
+ return new Date(Math.min(closest.getTime(), nextShiftStart.getTime()));
19843
+ }
19380
19844
  function isArray(obj) {
19381
19845
  if (Array.isArray) {
19382
19846
  return Array.isArray(obj);
@@ -23478,13 +23942,263 @@ function computeProjectWorkHours(calendars) {
23478
23942
  return { startHour: earliestTime(starts), endHour: latestTime(ends) };
23479
23943
  }
23480
23944
 
23481
- // src/internal/state/pipeline-context.ts
23482
- function buildPipelineContext(adapter, options) {
23483
- return {
23484
- calendars: adapter.calendarReader,
23485
- hierarchy: createStateBackedHierarchyReader(adapter),
23486
- activityReader: createStateBackedActivityReader(adapter),
23487
- dateFormat: options.dateFormat ?? "YYYY-MM-DD",
23945
+ // src/boundary/backend/constants.ts
23946
+ var DAY_MS = 24 * 60 * 60 * 1e3;
23947
+ var EXCEPTION_DATE_FORMAT_REGEX = /^(\d{4})[/-](\d{1,2})[/-](\d{1,2})/;
23948
+ var WEEKDAYS_PER_WEEK = 7;
23949
+ var CALENDAR_FALLBACK_WORKING_DAYS = [false, true, true, true, true, true, false];
23950
+ var WORKING_DAY_TRUE_VALUES = /* @__PURE__ */ new Set([
23951
+ "1",
23952
+ "true"
23953
+ ]);
23954
+ var DEFAULT_ACTIVITY_CRETER = ACTIVITY_PONDERATOR_CRITERION.DURATION;
23955
+ var DEFAULT_CONSTRAINT_TYPE = CONSTRAINT_TYPE.ASAP;
23956
+ var CONSTRAINT_LABEL_MAP = {
23957
+ ASAP: CONSTRAINT_TYPE.ASAP,
23958
+ ALAP: CONSTRAINT_TYPE.ALAP,
23959
+ SNET: CONSTRAINT_TYPE.SNET,
23960
+ SNLT: CONSTRAINT_TYPE.SNLT,
23961
+ FNET: CONSTRAINT_TYPE.FNET,
23962
+ FNLT: CONSTRAINT_TYPE.FNLT,
23963
+ MSO: CONSTRAINT_TYPE.MSO,
23964
+ MFO: CONSTRAINT_TYPE.MFO,
23965
+ "AS SOON AS POSSIBLE": CONSTRAINT_TYPE.ASAP,
23966
+ "AS LATE AS POSSIBLE": CONSTRAINT_TYPE.ALAP,
23967
+ "START NO EARLIER THAN": CONSTRAINT_TYPE.SNET,
23968
+ "START NO LATER THAN": CONSTRAINT_TYPE.SNLT,
23969
+ "FINISH NO EARLIER THAN": CONSTRAINT_TYPE.FNET,
23970
+ "FINISH NO LATER THAN": CONSTRAINT_TYPE.FNLT,
23971
+ "MUST START ON": CONSTRAINT_TYPE.MSO,
23972
+ "MUST FINISH ON": CONSTRAINT_TYPE.MFO
23973
+ };
23974
+
23975
+ // src/boundary/backend/normalize-calendar.ts
23976
+ function normalizeCalendars(schedulingCalendars, sectorCalendars, rawBaseCalendars, exceptionRange = null) {
23977
+ const hasSchedulingCalendars = schedulingCalendars !== void 0 && schedulingCalendars.length > 0;
23978
+ const source = hasSchedulingCalendars ? schedulingCalendars : sectorCalendars ?? [];
23979
+ const calendars = source.map(
23980
+ (calendar) => parseSchedulingCalendar(calendar, exceptionRange)
23981
+ );
23982
+ const baseCalendars = (rawBaseCalendars ?? []).map(
23983
+ (calendar) => parseBaseCalendar(calendar, exceptionRange)
23984
+ );
23985
+ return {
23986
+ calendars,
23987
+ baseCalendars,
23988
+ knownCalendarIds: new Set(calendars.map((calendar) => calendar.id)),
23989
+ defaultCalendarId: pickDefaultCalendarId(calendars)
23990
+ };
23991
+ }
23992
+ function pickDefaultCalendarId(calendars) {
23993
+ let firstCalendarId = null;
23994
+ for (const calendar of calendars) {
23995
+ firstCalendarId ??= calendar.id;
23996
+ if (calendar.is_default) return calendar.id;
23997
+ }
23998
+ return firstCalendarId;
23999
+ }
24000
+ function buildCalendar(raw, shape) {
24001
+ const workingMask = parseWorkingDays(raw.working_days);
24002
+ const { days, hours } = parseWorktimeDays(raw, workingMask);
24003
+ const dates = parseExceptionDays(
24004
+ shape.exceptionsSource,
24005
+ shape.exceptionRange
24006
+ );
24007
+ const worktime = { days, hours };
24008
+ if (dates) worktime.dates = dates;
24009
+ return {
24010
+ id: `${String(raw.id)}${shape.idSuffix}`,
24011
+ name: raw.name,
24012
+ is_default: shape.isDefault,
24013
+ ...shape.baseDefault !== void 0 ? { baseDefault: shape.baseDefault } : {},
24014
+ worktime
24015
+ };
24016
+ }
24017
+ function parseSchedulingCalendar(raw, exceptionRange) {
24018
+ const rawBaseDefault = raw.baseDefault;
24019
+ const baseDefault = typeof rawBaseDefault === "boolean" ? rawBaseDefault : void 0;
24020
+ return buildCalendar(raw, {
24021
+ idSuffix: "",
24022
+ isDefault: Boolean(raw.is_default),
24023
+ baseDefault,
24024
+ exceptionsSource: raw.exceptiondays,
24025
+ exceptionRange
24026
+ });
24027
+ }
24028
+ function parseBaseCalendar(raw, exceptionRange) {
24029
+ return buildCalendar(raw, {
24030
+ idSuffix: "-base",
24031
+ isDefault: false,
24032
+ baseDefault: Boolean(raw.is_default),
24033
+ exceptionsSource: raw.baseexceptiondays ?? raw.exceptiondays,
24034
+ exceptionRange
24035
+ });
24036
+ }
24037
+ function parseExceptionDays(exceptions, range) {
24038
+ if (!Array.isArray(exceptions) || exceptions.length === 0) return null;
24039
+ const map = {};
24040
+ for (const exception of exceptions) {
24041
+ if (!exception) continue;
24042
+ const rangeKeys = expandRangeToUtcKeys(
24043
+ exception.from_date,
24044
+ exception.to_date,
24045
+ range
24046
+ );
24047
+ if (rangeKeys.length === 0) continue;
24048
+ const value = exceptionToDateValue(exception);
24049
+ if (value === void 0) continue;
24050
+ for (const key of rangeKeys) map[key] = value;
24051
+ }
24052
+ return Object.keys(map).length > 0 ? map : null;
24053
+ }
24054
+ function expandRangeToUtcKeys(from, to, range) {
24055
+ const fromTs = parseExceptionDate(from);
24056
+ const toTs = parseExceptionDate(to);
24057
+ if (fromTs === null || toTs === null) return [];
24058
+ if (range && (toTs < range.start || fromTs > range.end)) return [];
24059
+ const keys = [];
24060
+ let cursor = range ? Math.max(fromTs, range.start) : fromTs;
24061
+ const last = range ? Math.min(toTs, range.end) : toTs;
24062
+ while (cursor <= last) {
24063
+ keys.push(String(cursor));
24064
+ cursor += DAY_MS;
24065
+ }
24066
+ return keys;
24067
+ }
24068
+ function parseExceptionDate(raw) {
24069
+ if (typeof raw !== "string") return null;
24070
+ const match = EXCEPTION_DATE_FORMAT_REGEX.exec(raw.trim());
24071
+ if (!match) return null;
24072
+ const year = Number(match[1]);
24073
+ const month = Number(match[2]);
24074
+ const day = Number(match[3]);
24075
+ const isValidDate4 = Number.isFinite(year) && Number.isFinite(month) && Number.isFinite(day);
24076
+ if (!isValidDate4) return null;
24077
+ return Date.UTC(year, month - 1, day);
24078
+ }
24079
+ function exceptionToDateValue(exception) {
24080
+ if (exception.workable === false) return false;
24081
+ const shifts = exception.shifts;
24082
+ if (Array.isArray(shifts) && shifts.length > 0) {
24083
+ const windows = extractExceptionWindows(shifts);
24084
+ if (windows.length > 0) return windows;
24085
+ }
24086
+ return void 0;
24087
+ }
24088
+ var TIME_OF_DAY = /^\d{1,2}:\d{2}$/;
24089
+ function extractExceptionWindows(shifts) {
24090
+ const windows = [];
24091
+ for (const shift of shifts) {
24092
+ const csvs = splitShiftString(shift?.shift_string);
24093
+ if (!csvs) continue;
24094
+ const isCompactWindow = !csvs.startsCsv.includes(",") && !csvs.endsCsv.includes(",");
24095
+ if (isCompactWindow) {
24096
+ const start = csvs.startsCsv.trim();
24097
+ const end = csvs.endsCsv.trim();
24098
+ const isUsableWindow = !isMissingValue(start) && !isMissingValue(end) && TIME_OF_DAY.test(start) && TIME_OF_DAY.test(end);
24099
+ if (isUsableWindow) windows.push(`${start}-${end}`);
24100
+ continue;
24101
+ }
24102
+ const perDay = shiftCsvToPerDayWindows(csvs.startsCsv, csvs.endsCsv);
24103
+ const firstWindow = perDay.find((window2) => window2 !== null);
24104
+ if (firstWindow) windows.push(firstWindow);
24105
+ }
24106
+ windows.sort(compareWindowByStart);
24107
+ return windows;
24108
+ }
24109
+ function windowStartMinutes(window2) {
24110
+ const dash = window2.indexOf("-");
24111
+ const start = dash > 0 ? window2.slice(0, dash) : window2;
24112
+ const [hour, minute] = start.split(":");
24113
+ return (Number(hour) || 0) * 60 + (Number(minute) || 0);
24114
+ }
24115
+ function compareWindowByStart(first, second) {
24116
+ return windowStartMinutes(first) - windowStartMinutes(second);
24117
+ }
24118
+ function parseWorktimeDays(raw, workingMask) {
24119
+ const windowsByDay = Array.from({ length: 7 }, () => []);
24120
+ const addCsvPair = (startsCsv, endsCsv) => {
24121
+ const perDay = shiftCsvToPerDayWindows(startsCsv, endsCsv);
24122
+ for (let i = 0; i < 7; i += 1) {
24123
+ const window2 = perDay[i];
24124
+ const dayWindows = windowsByDay[i];
24125
+ if (window2 && dayWindows) dayWindows.push(window2);
24126
+ }
24127
+ };
24128
+ const hasNestedShifts = Array.isArray(raw.shifts) && raw.shifts.length > 0;
24129
+ const hasFlatShift = typeof raw.shift_start === "string" && typeof raw.shift_end === "string";
24130
+ if (hasNestedShifts) {
24131
+ for (const shift of raw.shifts) {
24132
+ const csvs = splitShiftString(shift?.shift_string);
24133
+ if (csvs) addCsvPair(csvs.startsCsv, csvs.endsCsv);
24134
+ }
24135
+ } else if (hasFlatShift) {
24136
+ addCsvPair(raw.shift_start, raw.shift_end);
24137
+ }
24138
+ for (const dayWindows of windowsByDay) dayWindows.sort(compareWindowByStart);
24139
+ const firstWorkingWindows = windowsByDay.find(
24140
+ (windows, i) => Boolean(workingMask[i]) && windows.length > 0
24141
+ );
24142
+ const defaultHours = firstWorkingWindows ?? [];
24143
+ const days = workingMask.map((isWorking, i) => {
24144
+ if (!isWorking) return false;
24145
+ const dayWindows = windowsByDay[i];
24146
+ return dayWindows && dayWindows.length > 0 ? dayWindows : defaultHours;
24147
+ });
24148
+ return { days, hours: defaultHours };
24149
+ }
24150
+ function shiftCsvToPerDayWindows(startsCsv, endsCsv) {
24151
+ const empty = Array.from(
24152
+ { length: WEEKDAYS_PER_WEEK },
24153
+ () => null
24154
+ );
24155
+ if (typeof startsCsv !== "string" || typeof endsCsv !== "string") {
24156
+ return empty;
24157
+ }
24158
+ const starts = startsCsv.split(",").map((value) => value.trim());
24159
+ const ends = endsCsv.split(",").map((value) => value.trim());
24160
+ const isAlignedWeek = starts.length === ends.length && starts.length > 0 && starts.length <= WEEKDAYS_PER_WEEK;
24161
+ if (!isAlignedWeek) return empty;
24162
+ const windows = [...empty];
24163
+ for (let i = 0; i < starts.length; i += 1) {
24164
+ const start = starts[i];
24165
+ const end = ends[i];
24166
+ if (isMissingValue(start) || isMissingValue(end)) continue;
24167
+ windows[i] = `${start}-${end}`;
24168
+ }
24169
+ return windows;
24170
+ }
24171
+ function splitShiftString(raw) {
24172
+ if (typeof raw !== "string") return null;
24173
+ const dash = raw.indexOf("-");
24174
+ if (dash <= 0) return null;
24175
+ return {
24176
+ startsCsv: raw.slice(0, dash),
24177
+ endsCsv: raw.slice(dash + 1)
24178
+ };
24179
+ }
24180
+ function parseWorkingDays(csv) {
24181
+ if (typeof csv !== "string") return [...CALENDAR_FALLBACK_WORKING_DAYS];
24182
+ const parts = csv.split(",").map((value) => value.trim());
24183
+ if (parts.length !== WEEKDAYS_PER_WEEK) {
24184
+ return [...CALENDAR_FALLBACK_WORKING_DAYS];
24185
+ }
24186
+ return parts.map((part) => WORKING_DAY_TRUE_VALUES.has(part));
24187
+ }
24188
+ var MISSING_SHIFT_VALUES = /* @__PURE__ */ new Set(["false", "null"]);
24189
+ function isMissingValue(value) {
24190
+ return !value || MISSING_SHIFT_VALUES.has(value);
24191
+ }
24192
+
24193
+ // src/internal/state/pipeline-context.ts
24194
+ function buildPipelineContext(adapter, options) {
24195
+ return {
24196
+ calendars: adapter.calendarReader,
24197
+ hasCalendar: (calendarId) => options.calendars.has(calendarId),
24198
+ getDefaultCalendarId: () => pickDefaultCalendarId(options.calendars.values()),
24199
+ hierarchy: createStateBackedHierarchyReader(adapter),
24200
+ activityReader: createStateBackedActivityReader(adapter),
24201
+ dateFormat: options.dateFormat ?? "YYYY-MM-DD",
23488
24202
  customHours: options.customHours ?? {},
23489
24203
  hoursPerDay: options.hoursPerDay
23490
24204
  };
@@ -24767,36 +25481,6 @@ var SaveTracker = class _SaveTracker {
24767
25481
  }
24768
25482
  };
24769
25483
 
24770
- // src/boundary/backend/constants.ts
24771
- var DAY_MS = 24 * 60 * 60 * 1e3;
24772
- var EXCEPTION_DATE_FORMAT_REGEX = /^(\d{4})[/-](\d{1,2})[/-](\d{1,2})/;
24773
- var WEEKDAYS_PER_WEEK = 7;
24774
- var CALENDAR_FALLBACK_WORKING_DAYS = [false, true, true, true, true, true, false];
24775
- var WORKING_DAY_TRUE_VALUES = /* @__PURE__ */ new Set([
24776
- "1",
24777
- "true"
24778
- ]);
24779
- var DEFAULT_ACTIVITY_CRETER = ACTIVITY_PONDERATOR_CRITERION.DURATION;
24780
- var DEFAULT_CONSTRAINT_TYPE = CONSTRAINT_TYPE.ASAP;
24781
- var CONSTRAINT_LABEL_MAP = {
24782
- ASAP: CONSTRAINT_TYPE.ASAP,
24783
- ALAP: CONSTRAINT_TYPE.ALAP,
24784
- SNET: CONSTRAINT_TYPE.SNET,
24785
- SNLT: CONSTRAINT_TYPE.SNLT,
24786
- FNET: CONSTRAINT_TYPE.FNET,
24787
- FNLT: CONSTRAINT_TYPE.FNLT,
24788
- MSO: CONSTRAINT_TYPE.MSO,
24789
- MFO: CONSTRAINT_TYPE.MFO,
24790
- "AS SOON AS POSSIBLE": CONSTRAINT_TYPE.ASAP,
24791
- "AS LATE AS POSSIBLE": CONSTRAINT_TYPE.ALAP,
24792
- "START NO EARLIER THAN": CONSTRAINT_TYPE.SNET,
24793
- "START NO LATER THAN": CONSTRAINT_TYPE.SNLT,
24794
- "FINISH NO EARLIER THAN": CONSTRAINT_TYPE.FNET,
24795
- "FINISH NO LATER THAN": CONSTRAINT_TYPE.FNLT,
24796
- "MUST START ON": CONSTRAINT_TYPE.MSO,
24797
- "MUST FINISH ON": CONSTRAINT_TYPE.MFO
24798
- };
24799
-
24800
25484
  // src/boundary/backend/normalize-activity.ts
24801
25485
  function normalizeActivities(rawActivities, context) {
24802
25486
  const hiddenIds = [];
@@ -24915,332 +25599,115 @@ function parseBaselinePoints(raw, reporter, recordDiagnostic, activityId, isMile
24915
25599
  record: recordDiagnostic,
24916
25600
  sampleId: activityId
24917
25601
  },
24918
- isMilestone
24919
- )
24920
- ).filter((point) => point !== null);
24921
- }
24922
- function parseActivity(raw, context) {
24923
- const calendarId = resolveCalendarId2(
24924
- raw.calendarId,
24925
- context.knownCalendarIds,
24926
- context.defaultCalendarId
24927
- );
24928
- const inheritedDefaultCalendar = raw.calendarId == null || !context.knownCalendarIds.has(String(raw.calendarId));
24929
- const progress = parseProgress(
24930
- raw.progress,
24931
- context.reporter,
24932
- context.recordDiagnostic,
24933
- String(raw.unique_id)
24934
- );
24935
- const { constraintType, constraintDate } = resolveConstraint(
24936
- raw,
24937
- context.reporter,
24938
- context.recordDiagnostic
24939
- );
24940
- const isMilestone = raw.type === ACTIVITY_TYPE.MILESTONE;
24941
- const startDate = parseBackendDate(raw.start_date);
24942
- const endDate = isMilestone ? startDate : parseBackendDate(raw.end_date);
24943
- const durationDays = isMilestone ? 0 : Number(raw.duration);
24944
- const isCritical = parseCriticality(raw, context.reporter);
24945
- return {
24946
- id: String(raw.unique_id),
24947
- proplannerId: normalizePersistedActivityId(raw.id),
24948
- parentId: parseParentId(raw.parent_id),
24949
- name: raw.name,
24950
- description: raw.description ?? "",
24951
- type: raw.type,
24952
- startDate,
24953
- endDate,
24954
- durationHours: durationToHours(durationDays, context.hoursPerDay),
24955
- constraintType,
24956
- constraintDate,
24957
- autoScheduling: isAutoSchedulingEnabledForProgress(progress),
24958
- calendarId,
24959
- assignedDefaultCalendar: calendarId != null && inheritedDefaultCalendar,
24960
- dateOrigin: raw.date_origin == null ? null : parseBackendDate(raw.date_origin),
24961
- progress,
24962
- cost: Number(raw.cost),
24963
- usedCost: Number(raw.used_cost ?? 0),
24964
- realCost: Number(raw.real_cost ?? 0),
24965
- workHours: Number(raw.hhWorkTime),
24966
- realWorkHours: Number(raw.real_work ?? 0),
24967
- ponderator: Number(raw.ponderator ?? 0),
24968
- hasCustomPonderator: raw.hasCustomPonderator,
24969
- correlativeId: raw.correlative_id,
24970
- uniqueCorrelativeId: raw.unique_correlative_id,
24971
- customId: normalizeCustomId(raw),
24972
- subcontractId: raw.subcontractId,
24973
- isLookahead: raw.isOnLookahead,
24974
- hasLookaheadTasks: Array.isArray(raw.tasks) && raw.tasks.length > 0,
24975
- newActivityIds: parseNewActivityIds(raw.newActivitiesArray),
24976
- hasNewActivities: raw.hasNewActivities ?? false,
24977
- ...raw.isNewActivity === true ? { isNewActivity: true } : {},
24978
- pendingRequestIds: parseIdList(raw.pendingRequests),
24979
- responsableIds: parseIdList(raw.responsables),
24980
- tagIds: parseIdList(raw.tags),
24981
- baselinePoints: parseBaselinePoints(
24982
- raw.baseline_points,
24983
- context.reporter,
24984
- context.recordDiagnostic,
24985
- String(raw.unique_id),
24986
- isMilestone
24987
- ),
24988
- baselineSnapshot: null,
24989
- baselineVarianceDays: null,
24990
- recursiveDurationHours: durationToHours(
24991
- Number(raw.sumOfDurationRecursively ?? 0),
24992
- context.hoursPerDay
24993
- ),
24994
- calendarDuration: computeCalendarDuration({
24995
- type: raw.type,
24996
- startDate,
24997
- endDate
24998
- }),
24999
- isCritical,
25000
- freeSlackHours: null,
25001
- expectedProgress: null,
25002
- expectedProgressBaseline: null,
25003
- status: null,
25004
- criticalPath: null
25005
- };
25006
- }
25007
- function normalizeCustomId(raw) {
25008
- return raw.has_childs ? null : raw.custom_id;
25009
- }
25010
- function forceRootProjectToRoot(activities) {
25011
- const rootProject = activities.find(
25012
- (activity) => activity.correlativeId === 0 && activity.type === ACTIVITY_TYPE.PROJECT
25013
- );
25014
- if (rootProject) rootProject.parentId = null;
25015
- }
25016
- function reparentOrphansToRoot(activities, ids, reporter, recordDiagnostic) {
25017
- for (const activity of activities) {
25018
- if (activity.parentId === null) continue;
25019
- if (!ids.has(activity.parentId)) {
25020
- reporter.warn(
25021
- `[parseFromBackend] activity "${activity.id}" references unknown parent "${activity.parentId}" \u2014 re-parenting to root (null)`
25022
- );
25023
- recordDiagnostic(LOAD_DIAGNOSTIC_CODE.ORPHAN_ACTIVITY, activity.id);
25024
- activity.parentId = null;
25025
- }
25026
- }
25027
- }
25028
-
25029
- // src/boundary/backend/normalize-calendar.ts
25030
- function normalizeCalendars(schedulingCalendars, sectorCalendars, rawBaseCalendars, exceptionRange = null) {
25031
- const hasSchedulingCalendars = schedulingCalendars !== void 0 && schedulingCalendars.length > 0;
25032
- const source = hasSchedulingCalendars ? schedulingCalendars : sectorCalendars ?? [];
25033
- const calendars = source.map(
25034
- (calendar) => parseSchedulingCalendar(calendar, exceptionRange)
25035
- );
25036
- const baseCalendars = (rawBaseCalendars ?? []).map(
25037
- (calendar) => parseBaseCalendar(calendar, exceptionRange)
25038
- );
25039
- return {
25040
- calendars,
25041
- baseCalendars,
25042
- knownCalendarIds: new Set(calendars.map((calendar) => calendar.id)),
25043
- defaultCalendarId: pickDefaultCalendarId(calendars)
25044
- };
25045
- }
25046
- function pickDefaultCalendarId(calendars) {
25047
- const flaggedDefault = calendars.find((calendar) => calendar.is_default);
25048
- if (flaggedDefault) return flaggedDefault.id;
25049
- const firstCalendar = calendars[0];
25050
- if (firstCalendar) return firstCalendar.id;
25051
- return null;
25052
- }
25053
- function buildCalendar(raw, shape) {
25054
- const workingMask = parseWorkingDays(raw.working_days);
25055
- const { days, hours } = parseWorktimeDays(raw, workingMask);
25056
- const dates = parseExceptionDays(
25057
- shape.exceptionsSource,
25058
- shape.exceptionRange
25059
- );
25060
- const worktime = { days, hours };
25061
- if (dates) worktime.dates = dates;
25062
- return {
25063
- id: `${String(raw.id)}${shape.idSuffix}`,
25064
- name: raw.name,
25065
- is_default: shape.isDefault,
25066
- ...shape.baseDefault !== void 0 ? { baseDefault: shape.baseDefault } : {},
25067
- worktime
25068
- };
25069
- }
25070
- function parseSchedulingCalendar(raw, exceptionRange) {
25071
- const rawBaseDefault = raw.baseDefault;
25072
- const baseDefault = typeof rawBaseDefault === "boolean" ? rawBaseDefault : void 0;
25073
- return buildCalendar(raw, {
25074
- idSuffix: "",
25075
- isDefault: Boolean(raw.is_default),
25076
- baseDefault,
25077
- exceptionsSource: raw.exceptiondays,
25078
- exceptionRange
25079
- });
25080
- }
25081
- function parseBaseCalendar(raw, exceptionRange) {
25082
- return buildCalendar(raw, {
25083
- idSuffix: "-base",
25084
- isDefault: false,
25085
- baseDefault: Boolean(raw.is_default),
25086
- exceptionsSource: raw.baseexceptiondays ?? raw.exceptiondays,
25087
- exceptionRange
25088
- });
25089
- }
25090
- function parseExceptionDays(exceptions, range) {
25091
- if (!Array.isArray(exceptions) || exceptions.length === 0) return null;
25092
- const map = {};
25093
- for (const exception of exceptions) {
25094
- if (!exception) continue;
25095
- const rangeKeys = expandRangeToUtcKeys(
25096
- exception.from_date,
25097
- exception.to_date,
25098
- range
25099
- );
25100
- if (rangeKeys.length === 0) continue;
25101
- const value = exceptionToDateValue(exception);
25102
- if (value === void 0) continue;
25103
- for (const key of rangeKeys) map[key] = value;
25104
- }
25105
- return Object.keys(map).length > 0 ? map : null;
25106
- }
25107
- function expandRangeToUtcKeys(from, to, range) {
25108
- const fromTs = parseExceptionDate(from);
25109
- const toTs = parseExceptionDate(to);
25110
- if (fromTs === null || toTs === null) return [];
25111
- if (range && (toTs < range.start || fromTs > range.end)) return [];
25112
- const keys = [];
25113
- let cursor = range ? Math.max(fromTs, range.start) : fromTs;
25114
- const last = range ? Math.min(toTs, range.end) : toTs;
25115
- while (cursor <= last) {
25116
- keys.push(String(cursor));
25117
- cursor += DAY_MS;
25118
- }
25119
- return keys;
25120
- }
25121
- function parseExceptionDate(raw) {
25122
- if (typeof raw !== "string") return null;
25123
- const match = EXCEPTION_DATE_FORMAT_REGEX.exec(raw.trim());
25124
- if (!match) return null;
25125
- const year = Number(match[1]);
25126
- const month = Number(match[2]);
25127
- const day = Number(match[3]);
25128
- const isValidDate4 = Number.isFinite(year) && Number.isFinite(month) && Number.isFinite(day);
25129
- if (!isValidDate4) return null;
25130
- return Date.UTC(year, month - 1, day);
25131
- }
25132
- function exceptionToDateValue(exception) {
25133
- if (exception.workable === false) return false;
25134
- const shifts = exception.shifts;
25135
- if (Array.isArray(shifts) && shifts.length > 0) {
25136
- const windows = extractExceptionWindows(shifts);
25137
- if (windows.length > 0) return windows;
25138
- }
25139
- return void 0;
25140
- }
25141
- var TIME_OF_DAY = /^\d{1,2}:\d{2}$/;
25142
- function extractExceptionWindows(shifts) {
25143
- const windows = [];
25144
- for (const shift of shifts) {
25145
- const csvs = splitShiftString(shift?.shift_string);
25146
- if (!csvs) continue;
25147
- const isCompactWindow = !csvs.startsCsv.includes(",") && !csvs.endsCsv.includes(",");
25148
- if (isCompactWindow) {
25149
- const start = csvs.startsCsv.trim();
25150
- const end = csvs.endsCsv.trim();
25151
- const isUsableWindow = !isMissingValue(start) && !isMissingValue(end) && TIME_OF_DAY.test(start) && TIME_OF_DAY.test(end);
25152
- if (isUsableWindow) windows.push(`${start}-${end}`);
25153
- continue;
25154
- }
25155
- const perDay = shiftCsvToPerDayWindows(csvs.startsCsv, csvs.endsCsv);
25156
- const firstWindow = perDay.find((window2) => window2 !== null);
25157
- if (firstWindow) windows.push(firstWindow);
25158
- }
25159
- windows.sort(compareWindowByStart);
25160
- return windows;
25161
- }
25162
- function windowStartMinutes(window2) {
25163
- const dash = window2.indexOf("-");
25164
- const start = dash > 0 ? window2.slice(0, dash) : window2;
25165
- const [hour, minute] = start.split(":");
25166
- return (Number(hour) || 0) * 60 + (Number(minute) || 0);
25167
- }
25168
- function compareWindowByStart(first, second) {
25169
- return windowStartMinutes(first) - windowStartMinutes(second);
25170
- }
25171
- function parseWorktimeDays(raw, workingMask) {
25172
- const windowsByDay = Array.from({ length: 7 }, () => []);
25173
- const addCsvPair = (startsCsv, endsCsv) => {
25174
- const perDay = shiftCsvToPerDayWindows(startsCsv, endsCsv);
25175
- for (let i = 0; i < 7; i += 1) {
25176
- const window2 = perDay[i];
25177
- const dayWindows = windowsByDay[i];
25178
- if (window2 && dayWindows) dayWindows.push(window2);
25179
- }
25180
- };
25181
- const hasNestedShifts = Array.isArray(raw.shifts) && raw.shifts.length > 0;
25182
- const hasFlatShift = typeof raw.shift_start === "string" && typeof raw.shift_end === "string";
25183
- if (hasNestedShifts) {
25184
- for (const shift of raw.shifts) {
25185
- const csvs = splitShiftString(shift?.shift_string);
25186
- if (csvs) addCsvPair(csvs.startsCsv, csvs.endsCsv);
25187
- }
25188
- } else if (hasFlatShift) {
25189
- addCsvPair(raw.shift_start, raw.shift_end);
25190
- }
25191
- for (const dayWindows of windowsByDay) dayWindows.sort(compareWindowByStart);
25192
- const firstWorkingWindows = windowsByDay.find(
25193
- (windows, i) => Boolean(workingMask[i]) && windows.length > 0
25194
- );
25195
- const defaultHours = firstWorkingWindows ?? [];
25196
- const days = workingMask.map((isWorking, i) => {
25197
- if (!isWorking) return false;
25198
- const dayWindows = windowsByDay[i];
25199
- return dayWindows && dayWindows.length > 0 ? dayWindows : defaultHours;
25200
- });
25201
- return { days, hours: defaultHours };
25202
- }
25203
- function shiftCsvToPerDayWindows(startsCsv, endsCsv) {
25204
- const empty = Array.from(
25205
- { length: WEEKDAYS_PER_WEEK },
25206
- () => null
25207
- );
25208
- if (typeof startsCsv !== "string" || typeof endsCsv !== "string") {
25209
- return empty;
25210
- }
25211
- const starts = startsCsv.split(",").map((value) => value.trim());
25212
- const ends = endsCsv.split(",").map((value) => value.trim());
25213
- const isAlignedWeek = starts.length === ends.length && starts.length > 0 && starts.length <= WEEKDAYS_PER_WEEK;
25214
- if (!isAlignedWeek) return empty;
25215
- const windows = [...empty];
25216
- for (let i = 0; i < starts.length; i += 1) {
25217
- const start = starts[i];
25218
- const end = ends[i];
25219
- if (isMissingValue(start) || isMissingValue(end)) continue;
25220
- windows[i] = `${start}-${end}`;
25221
- }
25222
- return windows;
25602
+ isMilestone
25603
+ )
25604
+ ).filter((point) => point !== null);
25223
25605
  }
25224
- function splitShiftString(raw) {
25225
- if (typeof raw !== "string") return null;
25226
- const dash = raw.indexOf("-");
25227
- if (dash <= 0) return null;
25606
+ function parseActivity(raw, context) {
25607
+ const calendarId = resolveCalendarId2(
25608
+ raw.calendarId,
25609
+ context.knownCalendarIds,
25610
+ context.defaultCalendarId
25611
+ );
25612
+ const inheritedDefaultCalendar = raw.calendarId == null || !context.knownCalendarIds.has(String(raw.calendarId));
25613
+ const progress = parseProgress(
25614
+ raw.progress,
25615
+ context.reporter,
25616
+ context.recordDiagnostic,
25617
+ String(raw.unique_id)
25618
+ );
25619
+ const { constraintType, constraintDate } = resolveConstraint(
25620
+ raw,
25621
+ context.reporter,
25622
+ context.recordDiagnostic
25623
+ );
25624
+ const isMilestone = raw.type === ACTIVITY_TYPE.MILESTONE;
25625
+ const startDate = parseBackendDate(raw.start_date);
25626
+ const endDate = isMilestone ? startDate : parseBackendDate(raw.end_date);
25627
+ const durationDays = isMilestone ? 0 : Number(raw.duration);
25628
+ const isCritical = parseCriticality(raw, context.reporter);
25228
25629
  return {
25229
- startsCsv: raw.slice(0, dash),
25230
- endsCsv: raw.slice(dash + 1)
25630
+ id: String(raw.unique_id),
25631
+ proplannerId: normalizePersistedActivityId(raw.id),
25632
+ parentId: parseParentId(raw.parent_id),
25633
+ name: raw.name,
25634
+ description: raw.description ?? "",
25635
+ type: raw.type,
25636
+ startDate,
25637
+ endDate,
25638
+ durationHours: durationToHours(durationDays, context.hoursPerDay),
25639
+ constraintType,
25640
+ constraintDate,
25641
+ autoScheduling: isAutoSchedulingEnabledForProgress(progress),
25642
+ calendarId,
25643
+ assignedDefaultCalendar: calendarId != null && inheritedDefaultCalendar,
25644
+ dateOrigin: raw.date_origin == null ? null : parseBackendDate(raw.date_origin),
25645
+ progress,
25646
+ cost: Number(raw.cost),
25647
+ usedCost: Number(raw.used_cost ?? 0),
25648
+ realCost: Number(raw.real_cost ?? 0),
25649
+ workHours: Number(raw.hhWorkTime),
25650
+ realWorkHours: Number(raw.real_work ?? 0),
25651
+ ponderator: Number(raw.ponderator ?? 0),
25652
+ hasCustomPonderator: raw.hasCustomPonderator,
25653
+ correlativeId: raw.correlative_id,
25654
+ uniqueCorrelativeId: raw.unique_correlative_id,
25655
+ customId: normalizeCustomId(raw),
25656
+ subcontractId: raw.subcontractId,
25657
+ isLookahead: raw.isOnLookahead,
25658
+ hasLookaheadTasks: Array.isArray(raw.tasks) && raw.tasks.length > 0,
25659
+ newActivityIds: parseNewActivityIds(raw.newActivitiesArray),
25660
+ hasNewActivities: raw.hasNewActivities ?? false,
25661
+ ...raw.isNewActivity === true ? { isNewActivity: true } : {},
25662
+ pendingRequestIds: parseIdList(raw.pendingRequests),
25663
+ responsableIds: parseIdList(raw.responsables),
25664
+ tagIds: parseIdList(raw.tags),
25665
+ baselinePoints: parseBaselinePoints(
25666
+ raw.baseline_points,
25667
+ context.reporter,
25668
+ context.recordDiagnostic,
25669
+ String(raw.unique_id),
25670
+ isMilestone
25671
+ ),
25672
+ baselineSnapshot: null,
25673
+ baselineVarianceDays: null,
25674
+ recursiveDurationHours: durationToHours(
25675
+ Number(raw.sumOfDurationRecursively ?? 0),
25676
+ context.hoursPerDay
25677
+ ),
25678
+ calendarDuration: computeCalendarDuration({
25679
+ type: raw.type,
25680
+ startDate,
25681
+ endDate
25682
+ }),
25683
+ isCritical,
25684
+ freeSlackHours: null,
25685
+ expectedProgress: null,
25686
+ expectedProgressBaseline: null,
25687
+ status: null,
25688
+ criticalPath: null
25231
25689
  };
25232
25690
  }
25233
- function parseWorkingDays(csv) {
25234
- if (typeof csv !== "string") return [...CALENDAR_FALLBACK_WORKING_DAYS];
25235
- const parts = csv.split(",").map((value) => value.trim());
25236
- if (parts.length !== WEEKDAYS_PER_WEEK) {
25237
- return [...CALENDAR_FALLBACK_WORKING_DAYS];
25238
- }
25239
- return parts.map((part) => WORKING_DAY_TRUE_VALUES.has(part));
25691
+ function normalizeCustomId(raw) {
25692
+ return raw.has_childs ? null : raw.custom_id;
25240
25693
  }
25241
- var MISSING_SHIFT_VALUES = /* @__PURE__ */ new Set(["false", "null"]);
25242
- function isMissingValue(value) {
25243
- return !value || MISSING_SHIFT_VALUES.has(value);
25694
+ function forceRootProjectToRoot(activities) {
25695
+ const rootProject = activities.find(
25696
+ (activity) => activity.correlativeId === 0 && activity.type === ACTIVITY_TYPE.PROJECT
25697
+ );
25698
+ if (rootProject) rootProject.parentId = null;
25699
+ }
25700
+ function reparentOrphansToRoot(activities, ids, reporter, recordDiagnostic) {
25701
+ for (const activity of activities) {
25702
+ if (activity.parentId === null) continue;
25703
+ if (!ids.has(activity.parentId)) {
25704
+ reporter.warn(
25705
+ `[parseFromBackend] activity "${activity.id}" references unknown parent "${activity.parentId}" \u2014 re-parenting to root (null)`
25706
+ );
25707
+ recordDiagnostic(LOAD_DIAGNOSTIC_CODE.ORPHAN_ACTIVITY, activity.id);
25708
+ activity.parentId = null;
25709
+ }
25710
+ }
25244
25711
  }
25245
25712
 
25246
25713
  // src/boundary/backend/normalize-link.ts
@@ -26175,23 +26642,40 @@ function recomputeCanonicalRealWork(state) {
26175
26642
  state.forEachActivity((activity, activityId) => {
26176
26643
  if (activity.parentId === null) rootIds.push(String(activityId));
26177
26644
  });
26178
- const visit = (activityId) => {
26179
- const activity = state.getActivity(activityId);
26180
- if (!activity) return 0;
26181
- const childIds = [...state.getChildren(activityId)];
26182
- const realWork = childIds.length === 0 ? computeLeafRealWork(activity.workHours, activity.progress) : childIds.reduce(
26183
- (total, childId) => total + visit(String(childId)),
26184
- 0
26185
- );
26645
+ const stack = rootIds.map((activityId) => ({
26646
+ activityId,
26647
+ parent: null,
26648
+ childIds: null,
26649
+ realWork: 0
26650
+ })).reverse();
26651
+ for (let frame = stack.pop(); frame; frame = stack.pop()) {
26652
+ if (frame.childIds === null) {
26653
+ const activity = state.getActivity(frame.activityId);
26654
+ if (!activity) continue;
26655
+ const childIds = [...state.getChildren(frame.activityId)];
26656
+ const isLeaf = childIds.length === 0;
26657
+ frame.childIds = childIds.values();
26658
+ frame.realWork = isLeaf ? computeLeafRealWork(activity.workHours, activity.progress) : 0;
26659
+ }
26660
+ const nextChild = frame.childIds.next();
26661
+ const hasNextChild = !nextChild.done;
26662
+ if (hasNextChild) {
26663
+ stack.push(frame, {
26664
+ activityId: String(nextChild.value),
26665
+ parent: frame,
26666
+ childIds: null,
26667
+ realWork: 0
26668
+ });
26669
+ continue;
26670
+ }
26186
26671
  state.setActivityField(
26187
- activityId,
26672
+ frame.activityId,
26188
26673
  ACTIVITY_PROPERTY.REAL_WORK_HOURS,
26189
- realWork
26674
+ frame.realWork
26190
26675
  );
26191
- changedIds.push(activityId);
26192
- return realWork;
26193
- };
26194
- for (const rootId of rootIds) visit(rootId);
26676
+ changedIds.push(frame.activityId);
26677
+ if (frame.parent) frame.parent.realWork += frame.realWork;
26678
+ }
26195
26679
  return changedIds;
26196
26680
  }
26197
26681
 
@@ -26204,8 +26688,16 @@ async function runInitialPasses(deps) {
26204
26688
  }
26205
26689
  normalizeLoadedConstraintDates(state);
26206
26690
  recomputeCanonicalRealWork(state);
26691
+ if (sector.activityCreter === ACTIVITY_PONDERATOR_CRITERION.COST) {
26692
+ let hasActiveBaseline2 = false;
26693
+ state.forEachActivity((activity) => {
26694
+ hasActiveBaseline2 ||= getActiveBaseline(activity) !== null;
26695
+ });
26696
+ if (hasActiveBaseline2)
26697
+ recomputeAllPonderators(ACTIVITY_PONDERATOR_CRITERION.COST, state);
26698
+ }
26207
26699
  if (skipAutoSchedule) {
26208
- recomputeAllProgressRollup(state);
26700
+ recomputeAllProgressRollup(state, sector.activityCreter);
26209
26701
  if (now) {
26210
26702
  applyExpectedProgressLive(state, now);
26211
26703
  applyStatusPass(state, sector.statusCriteria);
@@ -26218,7 +26710,7 @@ async function runInitialPasses(deps) {
26218
26710
  normalizeMilestoneConstraintDatesForDisplay(state);
26219
26711
  recomputeSkippedLeafEnds(state);
26220
26712
  await updateParentBoundsFromChildren(state, void 0, false);
26221
- recomputeAllProgressRollup(state);
26713
+ recomputeAllProgressRollup(state, sector.activityCreter);
26222
26714
  if (now) {
26223
26715
  applyExpectedProgressLive(state, now);
26224
26716
  applyStatusPass(state, sector.statusCriteria);
@@ -26691,6 +27183,39 @@ function clampPercent(value) {
26691
27183
  return Math.min(MAXIMUM_PERCENT, Math.max(MINIMUM_PERCENT, value));
26692
27184
  }
26693
27185
 
27186
+ // src/p6/rolled-expected-finish.ts
27187
+ var PINNABLE_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
27188
+ P6_ACTIVITY_TYPE.TASK,
27189
+ P6_ACTIVITY_TYPE.RESOURCE_DEPENDENT
27190
+ ]);
27191
+ var NO_ROLLED_EXPECTED_FINISHES = /* @__PURE__ */ new Map();
27192
+ function planRolledExpectedFinishes(input) {
27193
+ const adoptsShownFinishes = input.settings.useExpectedFinishDates && rollsDataDateForward(input.previousSettings, input.settings);
27194
+ if (!adoptsShownFinishes) return NO_ROLLED_EXPECTED_FINISHES;
27195
+ const pinned = /* @__PURE__ */ new Map();
27196
+ for (const [activityId, state] of input.activities) {
27197
+ const shownFinish = state.remainingEarlyFinish;
27198
+ if (shownFinish === null) continue;
27199
+ if (!needsExpectedFinishPin(state)) continue;
27200
+ pinned.set(activityId, new Date(shownFinish));
27201
+ }
27202
+ return pinned;
27203
+ }
27204
+ function rollsDataDateForward(previous, next) {
27205
+ const previousDataDate = previous.dataDate;
27206
+ const nextDataDate = next.dataDate;
27207
+ if (previousDataDate === null || nextDataDate === null) return false;
27208
+ return nextDataDate.getTime() > previousDataDate.getTime();
27209
+ }
27210
+ function needsExpectedFinishPin(state) {
27211
+ return state.executionStatus === P6_EXECUTION_STATUS.IN_PROGRESS && state.expectedFinish === null && hasPinnableActivityType(state);
27212
+ }
27213
+ function hasPinnableActivityType(state) {
27214
+ const { activityType } = state;
27215
+ if (activityType === void 0) return true;
27216
+ return PINNABLE_ACTIVITY_TYPES.has(activityType);
27217
+ }
27218
+
26694
27219
  // src/p6/scheduler-projection.ts
26695
27220
  function buildDataDateAuthorityFields(p6, settings) {
26696
27221
  return {
@@ -26733,20 +27258,32 @@ function buildDataDateScheduleProjection(input) {
26733
27258
  }
26734
27259
 
26735
27260
  // src/p6/live-derived.ts
26736
- function prepareDataDateLiveDerived(source, before, after) {
27261
+ function prepareDataDateLiveDerived(source, before, after, options = {}) {
26737
27262
  if (!dataDateChanged(before.dataDate, after.dataDate)) return /* @__PURE__ */ new Map();
26738
27263
  if (!hasActiveDataDate(after)) return /* @__PURE__ */ new Map();
26739
27264
  const activating = !hasActiveDataDate(before);
27265
+ const activities = source.getAllActivities();
27266
+ const rolledExpectedFinishes = planSettingsExpectedFinishes(
27267
+ activities,
27268
+ before,
27269
+ after,
27270
+ options.adoptExpectedFinish
27271
+ );
26740
27272
  const patches = /* @__PURE__ */ new Map();
26741
- for (const activity of source.getAllActivities()) {
27273
+ for (const activity of activities) {
26742
27274
  const p6 = activity.p6;
26743
27275
  if (p6 === void 0) continue;
26744
27276
  const calendar = source.calendarReader.getCalendar(activity.calendarId);
26745
27277
  if (calendar === null) {
26746
27278
  throw new Error(`Missing calendar for Data Date activity ${activity.id}`);
26747
27279
  }
26748
- const nextP6 = liveDerivedState(p6, after.dataDate, calendar);
26749
- const p6Changed = !Object.is(nextP6.schedulePercentComplete, p6.schedulePercentComplete) || !Object.is(nextP6.actualDurationHours, p6.actualDurationHours);
27280
+ const nextP6 = liveDerivedState(
27281
+ p6,
27282
+ after.dataDate,
27283
+ calendar,
27284
+ rolledExpectedFinishes.get(activity.id)
27285
+ );
27286
+ const p6Changed = !Object.is(nextP6.expectedFinish, p6.expectedFinish) || !Object.is(nextP6.schedulePercentComplete, p6.schedulePercentComplete) || !Object.is(nextP6.actualDurationHours, p6.actualDurationHours);
26750
27287
  if (!activating && !p6Changed) continue;
26751
27288
  patches.set(activity.id, {
26752
27289
  ...p6Changed ? { p6: nextP6 } : {},
@@ -26755,17 +27292,37 @@ function prepareDataDateLiveDerived(source, before, after) {
26755
27292
  }
26756
27293
  return patches;
26757
27294
  }
27295
+ function planSettingsExpectedFinishes(activities, before, after, adoptExpectedFinish = false) {
27296
+ if (!adoptExpectedFinish) return /* @__PURE__ */ new Map();
27297
+ const overridesDataDateWithForecast = after.setDataDateAndPlannedStartToForecastStart && after.projectForecastStart !== null;
27298
+ if (overridesDataDateWithForecast) return /* @__PURE__ */ new Map();
27299
+ const scheduledActivities = new Map(
27300
+ activities.flatMap((activity) => {
27301
+ const p6 = activity.p6;
27302
+ const hasSchedulingState = p6 !== void 0 && isP6ActivityState(p6);
27303
+ if (activity.type === ACTIVITY_TYPE.PROJECT || !hasSchedulingState)
27304
+ return [];
27305
+ return [[activity.id, p6]];
27306
+ })
27307
+ );
27308
+ return planRolledExpectedFinishes({
27309
+ previousSettings: before,
27310
+ settings: after,
27311
+ activities: scheduledActivities
27312
+ });
27313
+ }
26758
27314
  function applyDataDateLiveDerived(writer, patches) {
26759
27315
  for (const [activityId, fields] of patches) {
26760
27316
  writer.setActivityFields(activityId, fields);
26761
27317
  }
26762
27318
  }
26763
- function liveDerivedState(p6, dataDate, calendar) {
27319
+ function liveDerivedState(p6, dataDate, calendar, rolledExpectedFinish) {
26764
27320
  const context = { dataDate, calendar };
26765
27321
  const schedulePercentComplete = deriveP6SchedulePercentComplete(p6, context);
26766
27322
  const measuresActualDuration = isP6ActivityState(p6) && p6.executionStatus === P6_EXECUTION_STATUS.IN_PROGRESS;
26767
27323
  return {
26768
27324
  ...p6,
27325
+ expectedFinish: rolledExpectedFinish ?? p6.expectedFinish,
26769
27326
  schedulePercentComplete,
26770
27327
  ...measuresActualDuration ? { actualDurationHours: deriveP6ActualDurationHours(p6, context) } : {}
26771
27328
  };
@@ -29207,8 +29764,8 @@ function compareActivityReference(leftId, rightId, adapter, settings) {
29207
29764
  function collectFreeFloatPaths(endingActivityId2, adapter, forward, settings) {
29208
29765
  const paths = [];
29209
29766
  const assigned = /* @__PURE__ */ new Set();
29210
- const visit = (pathEndId) => {
29211
- if (assigned.has(pathEndId)) return;
29767
+ const pending = [endingActivityId2];
29768
+ for (let pathEndId = pending.pop(); pathEndId !== void 0; pathEndId = pending.pop()) {
29212
29769
  const path = [];
29213
29770
  let currentId = pathEndId;
29214
29771
  while (currentId !== null && !assigned.has(currentId)) {
@@ -29218,23 +29775,24 @@ function collectFreeFloatPaths(endingActivityId2, adapter, forward, settings) {
29218
29775
  path.push(currentId);
29219
29776
  currentId = startsFreeFloatPathSegment(currentId, adapter, settings) ? null : resolvePrimaryPathPredecessor(currentId, adapter, forward, settings);
29220
29777
  }
29221
- if (path.length === 0) return;
29778
+ if (path.length === 0) continue;
29222
29779
  paths.push(path);
29223
- for (const activityId of path) {
29780
+ const alternatives = [...path].reverse().flatMap((activityId) => {
29224
29781
  const primaryPredecessor = startsFreeFloatPathSegment(
29225
29782
  activityId,
29226
29783
  adapter,
29227
29784
  settings
29228
29785
  ) ? null : resolvePrimaryPathPredecessor(activityId, adapter, forward, settings);
29229
- const alternatives = adapter.getIncomingLinkIds(activityId).map((linkId) => adapter.getLink(linkId)).filter(
29786
+ return adapter.getIncomingLinkIds(activityId).map((linkId) => adapter.getLink(linkId)).filter(
29230
29787
  (link) => link !== null && link.source !== primaryPredecessor
29231
29788
  ).sort(
29232
- (left, right) => left.source.localeCompare(right.source) || left.id.localeCompare(right.id)
29789
+ (left, right) => right.source.localeCompare(left.source) || right.id.localeCompare(left.id)
29233
29790
  );
29234
- for (const link of alternatives) visit(link.source);
29791
+ });
29792
+ for (const link of alternatives) {
29793
+ pending.push(link.source);
29235
29794
  }
29236
- };
29237
- visit(endingActivityId2);
29795
+ }
29238
29796
  return paths;
29239
29797
  }
29240
29798
  function startsFreeFloatPathSegment(activityId, adapter, settings) {
@@ -29378,6 +29936,103 @@ function resolvePrimaryPathPredecessor(activityId, adapter, forward, settings) {
29378
29936
  return resolveLongestPathPredecessors(activityId, adapter, forward, settings)[0] ?? null;
29379
29937
  }
29380
29938
 
29939
+ // src/p6/settings-enums.ts
29940
+ function validateP6ScheduleSettingEnums(settings) {
29941
+ validateChoice(settings.outOfSequencePolicy, "outOfSequencePolicy", [
29942
+ "retained_logic",
29943
+ "progress_override",
29944
+ "actual_dates"
29945
+ ]);
29946
+ validateChoice(settings.dependencyReference, "dependencyReference", [
29947
+ "correlative_id",
29948
+ "custom_id"
29949
+ ]);
29950
+ validateChoice(settings.startToStartLagBasis, "startToStartLagBasis", [
29951
+ "actual_start",
29952
+ "early_start"
29953
+ ]);
29954
+ validateChoice(settings.relationshipLagCalendar, "relationshipLagCalendar", [
29955
+ "predecessor",
29956
+ "successor",
29957
+ "project_default",
29958
+ "twenty_four_hour"
29959
+ ]);
29960
+ validateChoice(settings.totalFloatMethod, "totalFloatMethod", [
29961
+ "start_float",
29962
+ "finish_float",
29963
+ "smallest"
29964
+ ]);
29965
+ validateChoice(
29966
+ settings.criticalActivityMethod,
29967
+ "criticalActivityMethod",
29968
+ Object.values(P6_CRITICAL_ACTIVITY_METHOD)
29969
+ );
29970
+ validateChoice(settings.calculateFloatBasedOn, "calculateFloatBasedOn", [
29971
+ "each_project",
29972
+ "opened_projects"
29973
+ ]);
29974
+ }
29975
+ function validateChoice(value, field, allowed) {
29976
+ if (allowed.includes(value)) return;
29977
+ throw new Error(`Invalid P6 schedule settings: ${field}`);
29978
+ }
29979
+
29980
+ // src/p6/wbs-summary-activities.ts
29981
+ function prependAncestor(ancestorId, matches, requested) {
29982
+ return requested.has(ancestorId) ? { ancestorId, next: matches } : matches;
29983
+ }
29984
+ function cacheCycleMatches(path, cycleStart, requested, cache) {
29985
+ const cycle = path.slice(cycleStart).reverse();
29986
+ let matches = null;
29987
+ for (const ancestorId of cycle)
29988
+ matches = prependAncestor(ancestorId, matches, requested);
29989
+ for (const ancestorId of cycle) cache.set(ancestorId, matches);
29990
+ return matches;
29991
+ }
29992
+ function resolveAncestorMatches(parentId, byId, requested, cache) {
29993
+ const path = [];
29994
+ const positions = /* @__PURE__ */ new Map();
29995
+ let currentId = parentId;
29996
+ while (currentId !== null && !cache.has(currentId) && !positions.has(currentId)) {
29997
+ positions.set(currentId, path.length);
29998
+ path.push(currentId);
29999
+ currentId = byId.get(currentId)?.parentId ?? null;
30000
+ }
30001
+ let matches = currentId === null ? null : cache.get(currentId) ?? null;
30002
+ const cycleStart = currentId === null ? void 0 : positions.get(currentId);
30003
+ let lastIndex = path.length - 1;
30004
+ if (cycleStart !== void 0) {
30005
+ matches = cacheCycleMatches(path, cycleStart, requested, cache);
30006
+ lastIndex = cycleStart - 1;
30007
+ }
30008
+ for (let index = lastIndex; index >= 0; index -= 1) {
30009
+ const ancestorId = path[index];
30010
+ if (ancestorId === void 0) throw new Error("Missing WBS ancestor");
30011
+ matches = prependAncestor(ancestorId, matches, requested);
30012
+ cache.set(ancestorId, matches);
30013
+ }
30014
+ return cache.get(parentId) ?? null;
30015
+ }
30016
+ function groupWbsSummaryActivities(activities, byId, ancestorIds) {
30017
+ const groups = /* @__PURE__ */ new Map();
30018
+ if (ancestorIds.size === 0) return groups;
30019
+ const cache = /* @__PURE__ */ new Map();
30020
+ for (const activity of activities) {
30021
+ const isRealActivity = activity.p6 !== void 0 && activity.p6.activityType !== P6_ACTIVITY_TYPE.WBS_SUMMARY;
30022
+ if (!isRealActivity) continue;
30023
+ const parentId = activity.parentId;
30024
+ if (parentId === null) continue;
30025
+ let matches = resolveAncestorMatches(parentId, byId, ancestorIds, cache);
30026
+ while (matches !== null) {
30027
+ const bucket = groups.get(matches.ancestorId);
30028
+ if (bucket === void 0) groups.set(matches.ancestorId, [activity]);
30029
+ else bucket.push(activity);
30030
+ matches = matches.next;
30031
+ }
30032
+ }
30033
+ return groups;
30034
+ }
30035
+
29381
30036
  // src/p6/schedule.ts
29382
30037
  var EMPTY_LEVEL_OF_EFFORT_WINDOWS = {
29383
30038
  remainingEarlyStart: null,
@@ -29529,27 +30184,6 @@ function calculateP6Schedule(input) {
29529
30184
  ];
29530
30185
  return { activities, criticalPath, warnings };
29531
30186
  }
29532
- function groupActivitiesByWbsAncestor(all, byId) {
29533
- const childrenByAncestor = /* @__PURE__ */ new Map();
29534
- for (const activity of all) {
29535
- if (activity.p6 === void 0 || activity.p6.activityType === "wbs_summary") {
29536
- continue;
29537
- }
29538
- let currentId = activity.parentId;
29539
- const visited = /* @__PURE__ */ new Set();
29540
- while (currentId !== null && !visited.has(currentId)) {
29541
- visited.add(currentId);
29542
- const bucket = childrenByAncestor.get(currentId);
29543
- if (bucket === void 0) {
29544
- childrenByAncestor.set(currentId, [activity]);
29545
- } else {
29546
- bucket.push(activity);
29547
- }
29548
- currentId = byId.get(currentId)?.parentId ?? null;
29549
- }
29550
- }
29551
- return childrenByAncestor;
29552
- }
29553
30187
  function hasNoPrimaryConstraint(constraintType) {
29554
30188
  return (constraintType ?? CONSTRAINT_TYPE.ASAP) === CONSTRAINT_TYPE.ASAP;
29555
30189
  }
@@ -29860,11 +30494,20 @@ function settleIsolatedLevelOfEffort(current, scheduled, dataDate) {
29860
30494
  function calculateWbsSummaries(input) {
29861
30495
  const { adapter, settings, activities } = input;
29862
30496
  const all = adapter.getAllActivities();
30497
+ const summaries = sortedP6Activities(
30498
+ all.filter(
30499
+ (activity) => activity.p6?.activityType === P6_ACTIVITY_TYPE.WBS_SUMMARY
30500
+ )
30501
+ );
30502
+ const ancestorIds = new Set(
30503
+ summaries.flatMap(
30504
+ (summary) => summary.parentId === null ? [] : [summary.parentId]
30505
+ )
30506
+ );
30507
+ if (ancestorIds.size === 0) return;
29863
30508
  const byId = new Map(all.map((activity) => [activity.id, activity]));
29864
- const childrenByAncestor = groupActivitiesByWbsAncestor(all, byId);
29865
- for (const summary of sortedP6Activities(all).filter(
29866
- (activity) => activity.p6?.activityType === "wbs_summary"
29867
- )) {
30509
+ const childrenByAncestor = groupWbsSummaryActivities(all, byId, ancestorIds);
30510
+ for (const summary of summaries) {
29868
30511
  if (summary.parentId === null) continue;
29869
30512
  const children = childrenByAncestor.get(summary.parentId) ?? [];
29870
30513
  if (children.length === 0) continue;
@@ -29987,6 +30630,7 @@ function validateP6ScheduleSettings(settings) {
29987
30630
  if (endingActivityId2 !== null && typeof endingActivityId2 !== "string" && (typeof endingActivityId2 !== "number" || !Number.isFinite(endingActivityId2))) {
29988
30631
  throw new Error("Invalid P6 schedule settings: ending activity ID");
29989
30632
  }
30633
+ validateP6ScheduleSettingEnums(settings);
29990
30634
  }
29991
30635
  function sortedP6Activities(activities) {
29992
30636
  return activities.filter((activity) => activity.p6 !== void 0).sort((left, right) => String(left.id).localeCompare(String(right.id)));
@@ -30160,39 +30804,6 @@ function blockedActivities(code, message, activityIds, missingInputs = []) {
30160
30804
  };
30161
30805
  }
30162
30806
 
30163
- // src/p6/rolled-expected-finish.ts
30164
- var PINNABLE_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
30165
- P6_ACTIVITY_TYPE.TASK,
30166
- P6_ACTIVITY_TYPE.RESOURCE_DEPENDENT
30167
- ]);
30168
- var NO_ROLLED_EXPECTED_FINISHES = /* @__PURE__ */ new Map();
30169
- function planRolledExpectedFinishes(input) {
30170
- const adoptsShownFinishes = input.settings.useExpectedFinishDates && rollsDataDateForward(input.previousSettings, input.settings);
30171
- if (!adoptsShownFinishes) return NO_ROLLED_EXPECTED_FINISHES;
30172
- const pinned = /* @__PURE__ */ new Map();
30173
- for (const [activityId, state] of input.activities) {
30174
- const shownFinish = state.remainingEarlyFinish;
30175
- if (shownFinish === null) continue;
30176
- if (!needsExpectedFinishPin(state)) continue;
30177
- pinned.set(activityId, new Date(shownFinish));
30178
- }
30179
- return pinned;
30180
- }
30181
- function rollsDataDateForward(previous, next) {
30182
- const previousDataDate = previous.dataDate;
30183
- const nextDataDate = next.dataDate;
30184
- if (previousDataDate === null || nextDataDate === null) return false;
30185
- return nextDataDate.getTime() > previousDataDate.getTime();
30186
- }
30187
- function needsExpectedFinishPin(state) {
30188
- return state.executionStatus === P6_EXECUTION_STATUS.IN_PROGRESS && state.expectedFinish === null && hasPinnableActivityType(state);
30189
- }
30190
- function hasPinnableActivityType(state) {
30191
- const { activityType } = state;
30192
- if (activityType === void 0) return true;
30193
- return PINNABLE_ACTIVITY_TYPES.has(activityType);
30194
- }
30195
-
30196
30807
  // src/p6/commands.ts
30197
30808
  function dataDateScheduleReadiness(input) {
30198
30809
  const preflight = getP6SchedulePreflight({
@@ -30410,6 +31021,10 @@ function throwPreflight(preflight) {
30410
31021
  }
30411
31022
  function validateActivityInputPatch(patch) {
30412
31023
  validateDates2(patch);
31024
+ const executionStatus = patch.executionStatus;
31025
+ if ("executionStatus" in patch && executionStatus !== P6_EXECUTION_STATUS.NOT_STARTED && executionStatus !== P6_EXECUTION_STATUS.IN_PROGRESS && executionStatus !== P6_EXECUTION_STATUS.COMPLETED) {
31026
+ throw new Error("Invalid P6 activity input: execution status");
31027
+ }
30413
31028
  for (const value of [
30414
31029
  patch.remainingDurationHours,
30415
31030
  patch.originalDurationHours,
@@ -31069,9 +31684,7 @@ var ScheduleCore = class _ScheduleCore {
31069
31684
  this._activityClipboard.value = cloneDomainValue(
31070
31685
  source._activityClipboard.value
31071
31686
  );
31072
- this._cellClipboard.value = cloneDomainValue(
31073
- source._cellClipboard.value
31074
- );
31687
+ this._cellClipboard.value = cloneDomainValue(source._cellClipboard.value);
31075
31688
  this._undo = source._undo.fork();
31076
31689
  this._saveTracker = source._saveTracker.fork();
31077
31690
  this._scheduleRevision = source._scheduleRevision;
@@ -31242,6 +31855,7 @@ var ScheduleCore = class _ScheduleCore {
31242
31855
  const activity = readActivity(this.coreRuntime.state, id);
31243
31856
  if (!activity) return null;
31244
31857
  const context = buildPipelineContext(this.coreRuntime.state, {
31858
+ calendars: this.coreRuntime.calendars,
31245
31859
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
31246
31860
  dateFormat: this.coreRuntime.sector.dateFormat
31247
31861
  });
@@ -31276,6 +31890,7 @@ var ScheduleCore = class _ScheduleCore {
31276
31890
  const pipeline = findPipelineForColumn(column);
31277
31891
  if (!activity || !pipeline) return null;
31278
31892
  const context = buildPipelineContext(this.coreRuntime.state, {
31893
+ calendars: this.coreRuntime.calendars,
31279
31894
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
31280
31895
  dateFormat: this.coreRuntime.sector.dateFormat
31281
31896
  });
@@ -31836,7 +32451,8 @@ var ScheduleCore = class _ScheduleCore {
31836
32451
  const liveDerived = capability.prepareLiveDerived(
31837
32452
  this.coreRuntime.state,
31838
32453
  before,
31839
- after
32454
+ after,
32455
+ { adoptExpectedFinish: true }
31840
32456
  );
31841
32457
  const touchedIds = [...liveDerived.keys()];
31842
32458
  const beforeSnap = new Map(
@@ -32265,6 +32881,7 @@ var ScheduleCore = class _ScheduleCore {
32265
32881
  this.assertReady();
32266
32882
  return buildLookaheadConversionPlan(input, {
32267
32883
  adapter: this.coreRuntime.state,
32884
+ sector: this.getSector(),
32268
32885
  stateRevision: this._stateRevision
32269
32886
  });
32270
32887
  }
@@ -32312,6 +32929,16 @@ var ScheduleCore = class _ScheduleCore {
32312
32929
  });
32313
32930
  continue;
32314
32931
  }
32932
+ const manualSchedulingActive = hasActiveDataDate(
32933
+ this.coreRuntime.state.dataDateSettings()
32934
+ );
32935
+ if (manualSchedulingActive) {
32936
+ results.push({
32937
+ ok: false,
32938
+ reason: REJECTION_REASON.MANUAL_SCHEDULING_ACTIVE
32939
+ });
32940
+ continue;
32941
+ }
32315
32942
  const changes = emptyChangeSet2(action);
32316
32943
  results.push({ ok: true, changes });
32317
32944
  acceptedChangeSets.push(changes);
@@ -32413,6 +33040,7 @@ var ScheduleCore = class _ScheduleCore {
32413
33040
  };
32414
33041
  }
32415
33042
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
33043
+ calendars: this.coreRuntime.calendars,
32416
33044
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
32417
33045
  dateFormat: this.coreRuntime.sector.dateFormat
32418
33046
  });