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