@outbuild-company/schedule-core 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -835,49 +835,63 @@ function appendToBucket(index, key, link) {
835
835
  }
836
836
  index.set(key, [link]);
837
837
  }
838
- function filterToChainEndpoints(leaves, adapter, linksByActivity) {
838
+ function filterToChainEndpoints(leaves, adapter, linksByActivity, selectSourceLinks) {
839
839
  if (leaves.length <= 1) return leaves;
840
840
  const leafSet = new Set(leaves.map(String));
841
841
  const keptLeaves = [];
842
842
  for (const leafId of leaves) {
843
- if (!shouldSkipMidChainLeaf(leafId, leafSet, adapter, linksByActivity)) {
843
+ if (!shouldSkipMidChainLeaf(
844
+ leafId,
845
+ leafSet,
846
+ adapter,
847
+ linksByActivity,
848
+ selectSourceLinks
849
+ )) {
844
850
  keptLeaves.push(leafId);
845
851
  }
846
852
  }
847
853
  return keptLeaves.length > 0 ? keptLeaves : leaves;
848
854
  }
849
- function shouldSkipMidChainLeaf(leafId, leafSet, adapter, linksByActivity) {
855
+ function shouldSkipMidChainLeaf(leafId, leafSet, adapter, linksByActivity, selectSourceLinks) {
850
856
  const leafKey = String(leafId);
851
857
  const leafTask = adapter.getActivity(leafId);
852
858
  if (!leafTask) return false;
853
859
  if (leafTask.autoScheduling === false) return false;
854
860
  const linksOnThis = linksByActivity.get(leafKey) ?? [];
855
861
  for (const siblingLink of linksOnThis) {
856
- if (isSelfLink(siblingLink)) continue;
857
- const otherId = otherEndOf(siblingLink, leafKey);
858
- if (!leafSet.has(String(otherId))) continue;
859
- const siblingTask = adapter.getActivity(otherId);
860
- if (!siblingTask) continue;
861
- if (siblingTask.autoScheduling === false) continue;
862
- if (linkConnectsMidChain(siblingLink, leafId, leafTask, otherId, siblingTask)) {
862
+ if (isMidChainConnection(
863
+ siblingLink,
864
+ leafKey,
865
+ leafTask,
866
+ leafSet,
867
+ adapter,
868
+ selectSourceLinks
869
+ ))
863
870
  return true;
864
- }
865
871
  }
866
872
  return false;
867
873
  }
874
+ function isMidChainConnection(link, leafKey, leafTask, leafSet, adapter, selectSourceLinks) {
875
+ if (isSelfLink(link)) return false;
876
+ const relevantDirection = selectSourceLinks ? String(link.source) === leafKey : String(link.target) === leafKey;
877
+ if (!relevantDirection) return false;
878
+ const otherId = otherEndOf(link, leafKey);
879
+ if (!leafSet.has(String(otherId))) return false;
880
+ const siblingTask = adapter.getActivity(otherId);
881
+ return Boolean(
882
+ siblingTask?.autoScheduling !== false && siblingTask && linkConnectsMidChain(link, leafTask, siblingTask)
883
+ );
884
+ }
868
885
  function isSelfLink(link) {
869
886
  return link.source === link.target;
870
887
  }
871
888
  function otherEndOf(link, knownEndKey) {
872
889
  return String(link.source) === knownEndKey ? link.target : link.source;
873
890
  }
874
- function linkConnectsMidChain(link, leafId, leafTask, otherId, siblingTask) {
891
+ function linkConnectsMidChain(link, leafTask, siblingTask) {
875
892
  const lag = link.lag ?? 0;
876
893
  const absLag = Math.abs(lag);
877
- if (link.target === leafId && absLag <= leafTask.durationHours) return true;
878
- if (link.target === otherId && absLag <= siblingTask.durationHours)
879
- return true;
880
- return false;
894
+ return String(link.target) === String(siblingTask.id) && absLag <= siblingTask.durationHours || String(link.target) === String(leafTask.id) && absLag <= leafTask.durationHours;
881
895
  }
882
896
  function expandParentLinks(links, adapter) {
883
897
  const expanded = [];
@@ -886,116 +900,172 @@ function expandParentLinks(links, adapter) {
886
900
  for (const link of links) {
887
901
  const sourceIsSummary = isSummary2(link.source, adapter);
888
902
  const targetIsSummary = isSummary2(link.target, adapter);
889
- if (sourceIsSummary && targetIsSummary) continue;
890
- if (targetIsSummary && isDescendantOf(link.source, link.target, adapter) || sourceIsSummary && isDescendantOf(link.target, link.source, adapter)) {
903
+ if (isInternalSummaryLink(link, sourceIsSummary, targetIsSummary, adapter))
891
904
  continue;
892
- }
893
905
  if (!sourceIsSummary && !targetIsSummary) {
894
906
  expanded.push(link);
895
907
  continue;
896
908
  }
897
- let sourceIds = sourceIsSummary ? getLeafDescendants(link.source, adapter) : [link.source];
898
- let targetIds = targetIsSummary ? getLeafDescendants(link.target, adapter) : [link.target];
899
- if (sourceIsSummary) {
900
- sourceIds = keepBoundDefiningLeaves(sourceIds, false, adapter);
901
- }
902
- if (targetIsSummary) {
903
- const chainEndpoints = filterToChainEndpoints(
904
- targetIds,
905
- adapter,
906
- linksByActivity
907
- );
908
- if (link.type === LINK_TYPE.FINISH_TO_FINISH || link.type === LINK_TYPE.START_TO_FINISH) {
909
- virtualIdCounter = pushOffsetPreservingLinks(
910
- expanded,
911
- sourceIds,
912
- chainEndpoints,
913
- link,
914
- link.target,
915
- adapter,
916
- virtualIdCounter
917
- );
918
- continue;
919
- }
920
- targetIds = keepBoundDefiningLeaves(chainEndpoints, true, adapter);
921
- }
922
- virtualIdCounter = pushExpandedLinks(
909
+ const sourceIds = expandedEndpoints(
910
+ link.source,
911
+ sourceIsSummary,
912
+ adapter,
913
+ linksByActivity,
914
+ true
915
+ );
916
+ const targetIds = expandedEndpoints(
917
+ link.target,
918
+ targetIsSummary,
919
+ adapter,
920
+ linksByActivity,
921
+ false
922
+ );
923
+ virtualIdCounter = pushOffsetPreservingLinks(
923
924
  expanded,
924
925
  sourceIds,
925
926
  targetIds,
926
927
  link,
927
- virtualIdCounter
928
+ adapter,
929
+ virtualIdCounter,
930
+ linksByActivity
928
931
  );
929
932
  }
930
933
  return expanded;
931
934
  }
932
- function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, summaryId, adapter, virtualIdCounter) {
933
- const summary = adapter.getActivity(summaryId);
934
- const summaryStart = summary?.startDate;
935
- const summaryDuration = summary?.durationHours ?? 0;
936
- const userLag = originalLink.lag ?? 0;
937
- const source = adapter.getActivity(originalLink.source);
938
- let counter = virtualIdCounter;
939
- for (const srcId of sourceIds) {
940
- for (const tgtId of leafIds) {
941
- if (String(srcId) === String(tgtId)) continue;
942
- const leaf = adapter.getActivity(tgtId);
943
- const offTarget = summaryStart && leaf && source ? adapter.calculateDuration({
944
- startDate: summaryStart,
945
- endDate: leaf.startDate,
946
- task: source
947
- }) : 0;
948
- expanded.push({
949
- id: `virtual_${originalLink.id}_${counter}`,
950
- source: srcId,
951
- target: tgtId,
952
- type: originalLink.type,
953
- lag: originalLink.lag,
954
- _sourceLag: 0,
955
- _targetLag: -summaryDuration,
956
- _trueLag: userLag + offTarget
957
- });
958
- counter += 1;
959
- }
960
- }
961
- return counter;
935
+ function isInternalSummaryLink(link, sourceIsSummary, targetIsSummary, adapter) {
936
+ return targetIsSummary && isDescendantOf(link.source, link.target, adapter) || sourceIsSummary && isDescendantOf(link.target, link.source, adapter);
962
937
  }
963
- function keepBoundDefiningLeaves(leaves, startBound, adapter) {
964
- if (leaves.length <= 1) return leaves;
965
- let best = null;
966
- for (const id of leaves) {
967
- const a = adapter.getActivity(id);
968
- const d = startBound ? a?.startDate : a?.endDate;
969
- if (!d) continue;
970
- const t = d.getTime();
971
- if (best === null) best = t;
972
- else if (startBound ? t < best : t > best) best = t;
973
- }
974
- if (best === null) return leaves;
975
- const kept = leaves.filter((id) => {
976
- const a = adapter.getActivity(id);
977
- const d = startBound ? a?.startDate : a?.endDate;
978
- return d != null && d.getTime() === best;
938
+ function expandedEndpoints(activityId, summary, adapter, linksByActivity, sourceSide) {
939
+ if (!summary) return [activityId];
940
+ return filterToChainEndpoints(
941
+ getLeafDescendants(activityId, adapter),
942
+ adapter,
943
+ linksByActivity,
944
+ sourceSide
945
+ );
946
+ }
947
+ function deriveSummaryBoundsWithDates(summaryId, adapter, resolveDates) {
948
+ const leaves = getLeafDescendants(summaryId, adapter);
949
+ if (leaves.length === 0) return null;
950
+ let earliestStart = null;
951
+ let latestEnd = null;
952
+ for (const leafId of leaves) {
953
+ const leafDates = resolveDates(leafId);
954
+ if (!leafDates) continue;
955
+ const startsEarlier = earliestStart === null || leafDates.startDate < earliestStart;
956
+ if (startsEarlier) earliestStart = leafDates.startDate;
957
+ const endsLater = latestEnd === null || leafDates.endDate > latestEnd;
958
+ if (endsLater) latestEnd = leafDates.endDate;
959
+ }
960
+ if (earliestStart === null || latestEnd === null) return null;
961
+ return { startDate: earliestStart, endDate: latestEnd };
962
+ }
963
+ function deriveSummaryBounds(summaryId, adapter) {
964
+ return deriveSummaryBoundsWithDates(summaryId, adapter, (leafId) => {
965
+ const leaf = adapter.getActivity(leafId);
966
+ if (!leaf) return null;
967
+ return { startDate: leaf.startDate, endDate: leaf.endDate };
968
+ });
969
+ }
970
+ function endpointSpanHours(endpoint, bounds, adapter) {
971
+ if (!endpoint) return 0;
972
+ if (!bounds) return endpoint.durationHours;
973
+ return adapter.calculateDuration({
974
+ startDate: bounds.startDate,
975
+ endDate: bounds.endDate,
976
+ task: endpoint
979
977
  });
980
- return kept.length > 0 ? kept : leaves;
981
978
  }
982
- function pushExpandedLinks(expanded, sourceIds, targetIds, originalLink, virtualIdCounter) {
979
+ function sourceLagForLeaf(leafId, source, sourceBounds, adapter) {
980
+ if (!source) return 0;
981
+ const leaf = adapter.getActivity(leafId);
982
+ if (!sourceBounds || !leaf) return -source.durationHours;
983
+ return -adapter.calculateDuration({
984
+ startDate: sourceBounds.startDate,
985
+ endDate: leaf.endDate,
986
+ task: source
987
+ });
988
+ }
989
+ function hasIncomingRawLink(leafId, linksByActivity) {
990
+ const leafKey = String(leafId);
991
+ const bucket = linksByActivity.get(leafKey) ?? [];
992
+ return bucket.some((rawLink) => String(rawLink.target) === leafKey);
993
+ }
994
+ function targetOffsetForLeaf(leafId, targetStart, source, adapter, linksByActivity) {
995
+ if (hasIncomingRawLink(leafId, linksByActivity)) return 0;
996
+ const leaf = adapter.getActivity(leafId);
997
+ if (!targetStart || !leaf || !source) return 0;
998
+ return adapter.calculateDuration({
999
+ startDate: targetStart,
1000
+ endDate: leaf.startDate,
1001
+ task: source
1002
+ });
1003
+ }
1004
+ function resolveExpansionAnchors(originalLink, adapter) {
1005
+ const source = adapter.getActivity(originalLink.source);
1006
+ const target = adapter.getActivity(originalLink.target);
1007
+ const sourceBounds = deriveSummaryBounds(originalLink.source, adapter);
1008
+ const targetBounds = deriveSummaryBounds(originalLink.target, adapter);
1009
+ const sourceEndpointIsStart = originalLink.type === LINK_TYPE.START_TO_START || originalLink.type === LINK_TYPE.START_TO_FINISH;
1010
+ const targetEndpointIsFinish = originalLink.type === LINK_TYPE.FINISH_TO_FINISH || originalLink.type === LINK_TYPE.START_TO_FINISH;
1011
+ return {
1012
+ source,
1013
+ sourceBounds,
1014
+ targetStart: targetBounds?.startDate ?? target?.startDate,
1015
+ userLag: originalLink.lag ?? 0,
1016
+ sourceEndpointIsStart,
1017
+ targetLag: targetEndpointIsFinish ? -endpointSpanHours(target, targetBounds, adapter) : 0,
1018
+ summarySourceId: originalLink.type === LINK_TYPE.START_TO_START && sourceBounds ? originalLink.source : void 0
1019
+ };
1020
+ }
1021
+ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, adapter, virtualIdCounter, linksByActivity) {
1022
+ const anchors = resolveExpansionAnchors(originalLink, adapter);
1023
+ const { source, sourceBounds, targetStart, userLag } = anchors;
1024
+ const { sourceEndpointIsStart, targetLag, summarySourceId } = anchors;
983
1025
  let counter = virtualIdCounter;
984
1026
  for (const srcId of sourceIds) {
985
- for (const tgtId of targetIds) {
1027
+ const sourceLag = sourceEndpointIsStart ? sourceLagForLeaf(srcId, source, sourceBounds, adapter) : 0;
1028
+ for (const tgtId of leafIds) {
986
1029
  if (String(srcId) === String(tgtId)) continue;
987
- expanded.push({
988
- id: `virtual_${originalLink.id}_${counter}`,
989
- source: srcId,
990
- target: tgtId,
991
- type: originalLink.type,
992
- lag: originalLink.lag
993
- });
1030
+ const offTarget = targetOffsetForLeaf(
1031
+ tgtId,
1032
+ targetStart,
1033
+ source,
1034
+ adapter,
1035
+ linksByActivity
1036
+ );
1037
+ expanded.push(
1038
+ buildVirtualLink(
1039
+ originalLink,
1040
+ srcId,
1041
+ tgtId,
1042
+ counter,
1043
+ sourceLag,
1044
+ targetLag,
1045
+ userLag + offTarget,
1046
+ summarySourceId,
1047
+ offTarget !== 0 && targetLag === 0 ? originalLink.target : void 0
1048
+ )
1049
+ );
994
1050
  counter += 1;
995
1051
  }
996
1052
  }
997
1053
  return counter;
998
1054
  }
1055
+ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag, targetLag, trueLag, summarySourceId, offsetSummaryTargetId) {
1056
+ return {
1057
+ id: `virtual_${originalLink.id}_${counter}`,
1058
+ source: sourceId,
1059
+ target: targetId,
1060
+ type: originalLink.type,
1061
+ lag: originalLink.lag,
1062
+ _sourceLag: sourceLag,
1063
+ _targetLag: targetLag,
1064
+ _trueLag: trueLag,
1065
+ ...summarySourceId === void 0 ? {} : { _summarySourceId: summarySourceId },
1066
+ ...offsetSummaryTargetId === void 0 ? {} : { _summaryTargetId: offsetSummaryTargetId }
1067
+ };
1068
+ }
999
1069
 
1000
1070
  // src/autoscheduler/utils/expanded-links-cache.ts
1001
1071
  var ExpandedLinksCache = class {
@@ -1125,33 +1195,49 @@ function normalize(predecessor, successor, link) {
1125
1195
  };
1126
1196
  }
1127
1197
  }
1128
- function calculateSuccessorStartFromLink(predecessor, successor, link, adapter) {
1129
- const { sourceLag, targetLag, trueLag, isFS, isFF } = normalize(
1130
- predecessor,
1131
- successor,
1198
+ function summaryAnchoredSourceLag(link, predecessor, adapter) {
1199
+ if (link._summarySourceId === void 0) return null;
1200
+ const summarySource = adapter.getActivity(link._summarySourceId);
1201
+ if (!summarySource) return null;
1202
+ const summaryBounds = deriveSummaryBounds(link._summarySourceId, adapter);
1203
+ if (!summaryBounds) return null;
1204
+ return -adapter.calculateDuration({
1205
+ startDate: summaryBounds.startDate,
1206
+ endDate: predecessor.endDate,
1207
+ task: summarySource
1208
+ });
1209
+ }
1210
+ function noLagSuccessorStart(predecessor, successor, link, adapter, isEndAnchoredType) {
1211
+ const snapsToPast = successor.durationHours === 0 && isEndAnchoredType;
1212
+ return {
1213
+ successorStart: adapter.getClosestWorkTime({
1214
+ date: predecessor.endDate,
1215
+ dir: snapsToPast ? "past" : "future",
1216
+ task: successor
1217
+ }),
1132
1218
  link
1133
- );
1134
- const hasLag = sourceLag !== 0 || targetLag !== 0 || trueLag !== 0;
1219
+ };
1220
+ }
1221
+ function calculateSuccessorStartFromLink(predecessor, successor, link, adapter) {
1222
+ const {
1223
+ sourceLag: frozenSourceLag,
1224
+ targetLag,
1225
+ trueLag,
1226
+ isFS,
1227
+ isFF
1228
+ } = normalize(predecessor, successor, link);
1229
+ const sourceLag = summaryAnchoredSourceLag(link, predecessor, adapter) ?? frozenSourceLag;
1230
+ const totalLag = sourceLag + targetLag + trueLag;
1231
+ const isZeroDurationSuccessor = successor.durationHours === 0;
1232
+ const hasLag = isZeroDurationSuccessor ? totalLag !== 0 : sourceLag !== 0 || targetLag !== 0 || trueLag !== 0;
1135
1233
  if (!hasLag) {
1136
- const isFForFS = isFS || isFF;
1137
- if (successor.durationHours === 0 && isFForFS) {
1138
- return {
1139
- successorStart: adapter.getClosestWorkTime({
1140
- date: predecessor.endDate,
1141
- dir: "past",
1142
- task: successor
1143
- }),
1144
- link
1145
- };
1146
- }
1147
- return {
1148
- successorStart: adapter.getClosestWorkTime({
1149
- date: predecessor.endDate,
1150
- dir: "future",
1151
- task: successor
1152
- }),
1153
- link
1154
- };
1234
+ return noLagSuccessorStart(
1235
+ predecessor,
1236
+ successor,
1237
+ link,
1238
+ adapter,
1239
+ isFS || isFF
1240
+ );
1155
1241
  }
1156
1242
  const baseTask = isFS ? successor : predecessor;
1157
1243
  let date2 = adapter.getClosestWorkTime({
@@ -1372,10 +1458,12 @@ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
1372
1458
  const incoming = incomingLinks.get(String(activityId)) ?? [];
1373
1459
  let maxStart = null;
1374
1460
  let drivingLinkId = null;
1461
+ let drivingOffsetIsStale = false;
1375
1462
  for (const link of incoming) {
1376
1463
  const predecessorSnap = adapter.getActivity(link.source);
1377
1464
  if (!predecessorSnap) continue;
1378
1465
  const predDates = getActivityDates(link.source);
1466
+ const predecessorMovedThisRun = predDates.startDate.getTime() !== predecessorSnap.startDate.getTime() || predDates.endDate.getTime() !== predecessorSnap.endDate.getTime();
1379
1467
  const effectivePred = {
1380
1468
  ...predecessorSnap,
1381
1469
  startDate: predDates.startDate,
@@ -1405,8 +1493,13 @@ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
1405
1493
  if (!maxStart || result.successorStart > maxStart) {
1406
1494
  maxStart = result.successorStart;
1407
1495
  drivingLinkId = String(link.id);
1496
+ drivingOffsetIsStale = link._summaryTargetId !== void 0 && !predecessorMovedThisRun;
1408
1497
  }
1409
1498
  }
1499
+ const staleOffsetWouldPullBack = maxStart !== null && drivingOffsetIsStale && maxStart < activity.startDate;
1500
+ if (staleOffsetWouldPullBack) {
1501
+ maxStart = new Date(activity.startDate);
1502
+ }
1410
1503
  const existing = plans.get(activityId);
1411
1504
  const plan = existing ? { ...existing } : {
1412
1505
  activityId,
@@ -1574,31 +1667,33 @@ async function alapPass(reversedIds, links, asapPlans, adapter, _options, isCurr
1574
1667
  latestEnd = boundary;
1575
1668
  }
1576
1669
  }
1670
+ const incomingForKind = incomingLinks.get(String(activityId)) ?? [];
1671
+ const isFinishMarker = incomingForKind.some(
1672
+ (incomingLink) => incomingLink.type === LINK_TYPE.FINISH_TO_START || incomingLink.type === LINK_TYPE.FINISH_TO_FINISH
1673
+ );
1674
+ const latestEndAtDayStart = new Date(
1675
+ Date.UTC(
1676
+ latestEnd.getUTCFullYear(),
1677
+ latestEnd.getUTCMonth(),
1678
+ latestEnd.getUTCDate()
1679
+ )
1680
+ );
1681
+ const finishMarkerEnd = () => adapter.getClosestWorkTime({
1682
+ date: latestEndAtDayStart,
1683
+ dir: "past",
1684
+ task: activity
1685
+ });
1686
+ const latestEndLandsInsideWorkingDay = adapter.getClosestWorkTime({ date: latestEnd, dir: "future", task: activity }).getTime() === latestEnd.getTime();
1577
1687
  let snappedEnd;
1578
1688
  if (activity.durationHours === 0 && successorAnchor === null) {
1579
- snappedEnd = adapter.getClosestWorkTime({
1689
+ snappedEnd = isFinishMarker && latestEndLandsInsideWorkingDay ? finishMarkerEnd() : adapter.getClosestWorkTime({
1580
1690
  date: latestEnd,
1581
1691
  dir: "past",
1582
1692
  task: activity
1583
1693
  });
1584
1694
  } else if (activity.durationHours === 0) {
1585
- const incoming = incomingLinks.get(String(activityId)) ?? [];
1586
- const isFinishKind = incoming.some(
1587
- (l) => l.type === LINK_TYPE.FINISH_TO_START || l.type === LINK_TYPE.FINISH_TO_FINISH
1588
- );
1589
- if (isFinishKind) {
1590
- const dayStart = new Date(
1591
- Date.UTC(
1592
- latestEnd.getUTCFullYear(),
1593
- latestEnd.getUTCMonth(),
1594
- latestEnd.getUTCDate()
1595
- )
1596
- );
1597
- snappedEnd = adapter.getClosestWorkTime({
1598
- date: dayStart,
1599
- dir: "past",
1600
- task: activity
1601
- });
1695
+ if (isFinishMarker) {
1696
+ snappedEnd = finishMarkerEnd();
1602
1697
  } else {
1603
1698
  snappedEnd = adapter.getClosestWorkTime({
1604
1699
  date: latestEnd,
@@ -1631,10 +1726,16 @@ async function alapPass(reversedIds, links, asapPlans, adapter, _options, isCurr
1631
1726
  plan.endDate = snappedEnd;
1632
1727
  plan.drivingLinkId = null;
1633
1728
  plan.kind = "alap";
1729
+ const predecessorFloor = plan.earliestSchedulingStart ?? null;
1634
1730
  plan.earliestSchedulingStart = null;
1635
1731
  plan.earliestSchedulingEnd = null;
1636
- plan.latestSchedulingStart = snappedStart;
1637
- plan.latestSchedulingEnd = snappedEnd;
1732
+ const alapTarget = predecessorFloor !== null && predecessorFloor.getTime() > snappedStart.getTime() ? predecessorFloor : snappedStart;
1733
+ plan.latestSchedulingStart = alapTarget;
1734
+ plan.latestSchedulingEnd = alapTarget === snappedStart ? snappedEnd : adapter.calculateEndDate({
1735
+ startDate: alapTarget,
1736
+ durationHours: activity.durationHours,
1737
+ task: activity
1738
+ });
1638
1739
  limitPlanDates(activity, plan, adapter);
1639
1740
  plans.set(activityId, plan);
1640
1741
  }
@@ -2069,18 +2170,9 @@ var AutoScheduler = class {
2069
2170
  }
2070
2171
  const phaseScope = startPhase("schedule.scoping");
2071
2172
  const allIds = this.adapter.getAllIds();
2072
- let scopedLinks = activeLinks;
2073
- let scopedIds = allIds;
2074
- let scopedIdSet = null;
2075
- if (options.triggerId !== void 0 && options.triggerId !== null) {
2076
- const group = findConnectedGroupForTrigger(
2077
- options.triggerId,
2078
- activeLinks
2079
- );
2080
- scopedLinks = group.links;
2081
- scopedIds = Array.from(group.activityIds);
2082
- scopedIdSet = new Set(scopedIds);
2083
- }
2173
+ const scope = resolveScope(options, activeLinks, this.adapter);
2174
+ const scopedLinks = scope.links;
2175
+ const scopedIdSet = scope.idSet;
2084
2176
  endPhase(phaseScope);
2085
2177
  const phaseTopo = startPhase("schedule.topologicalSort");
2086
2178
  const topoResult = resolveTopoResult(
@@ -2177,6 +2269,34 @@ function endPhase(handle) {
2177
2269
  const elapsed = performance.now() - handle.startedAt;
2178
2270
  console.log(`[scheduler] ${handle.name} ${elapsed.toFixed(1)}ms`);
2179
2271
  }
2272
+ function resolveScope(options, activeLinks, adapter) {
2273
+ const triggerId = options.triggerId;
2274
+ if (triggerId === void 0 || triggerId === null) {
2275
+ return { idSet: null, links: activeLinks };
2276
+ }
2277
+ const group = findConnectedGroupForTrigger(triggerId, activeLinks);
2278
+ return widenScopeToAlapActivities(group, activeLinks, adapter);
2279
+ }
2280
+ function widenScopeToAlapActivities(group, allLinks, adapter) {
2281
+ const idSet = new Set(group.activityIds);
2282
+ const widenedAlapKeys = /* @__PURE__ */ new Set();
2283
+ for (const activity of adapter.getAllActivities()) {
2284
+ const activityKey = String(activity.id);
2285
+ const isOutOfComponentAlap = activity.constraintType === CONSTRAINT_TYPE2.ALAP && !idSet.has(activityKey);
2286
+ if (!isOutOfComponentAlap) continue;
2287
+ widenedAlapKeys.add(activityKey);
2288
+ idSet.add(activityKey);
2289
+ }
2290
+ if (widenedAlapKeys.size === 0) {
2291
+ return { idSet, links: group.links };
2292
+ }
2293
+ const links = [...group.links];
2294
+ for (const link of allLinks) {
2295
+ const touchesWidenedAlap = widenedAlapKeys.has(String(link.source)) || widenedAlapKeys.has(String(link.target));
2296
+ if (touchesWidenedAlap) links.push(link);
2297
+ }
2298
+ return { idSet, links };
2299
+ }
2180
2300
  function resolveTopoResult(cache, allIds, allLinks, scopedIdSet) {
2181
2301
  const fullTopo = cache.get(allIds, allLinks);
2182
2302
  if (fullTopo.hasCycles) {
@@ -2194,9 +2314,10 @@ function resolveTopoResult(cache, allIds, allLinks, scopedIdSet) {
2194
2314
  }
2195
2315
 
2196
2316
  // src/internal/post-processors/adjust-link-lag-on-task-move.ts
2197
- function adjustLinkLagOnTaskMove(targetId, adapter) {
2317
+ function adjustLinkLagOnTaskMove(targetId, adapter, preEditTarget) {
2198
2318
  const target = adapter.getActivity(targetId);
2199
2319
  if (!target) return;
2320
+ const targetForLagMath = preEditTarget ? { ...target, endDate: preEditTarget.endDate } : target;
2200
2321
  const calendarApi = adapter.calendarReader.getCalendar(
2201
2322
  String(target.calendarId)
2202
2323
  );
@@ -2206,9 +2327,11 @@ function adjustLinkLagOnTaskMove(targetId, adapter) {
2206
2327
  const source = adapter.getActivity(link.source);
2207
2328
  if (!source) continue;
2208
2329
  if (source.constraintType === "alap") continue;
2330
+ const isEndAnchoredIntoMilestone = isMilestoneType(target.type) && (link.type === LINK_TYPE.START_TO_FINISH || link.type === LINK_TYPE.FINISH_TO_FINISH);
2331
+ if (isEndAnchoredIntoMilestone) continue;
2209
2332
  const { sourceDate, targetDate } = pickDatesForLinkType(
2210
2333
  source,
2211
- target,
2334
+ targetForLagMath,
2212
2335
  link.type
2213
2336
  );
2214
2337
  const newLag = dateMath.computeLagBetweenTasks(
@@ -2256,20 +2379,15 @@ function computeLeafRealWork(workHours, progress) {
2256
2379
  // src/propagations/upward/parent-bounds.ts
2257
2380
  async function updateParentBoundsFromChildren(adapter, dirtyIds, recomputeProgress = true, options = {}) {
2258
2381
  const parents = dirtyIds ? findAncestorsOfDirty(adapter, dirtyIds) : findAllParents(adapter);
2259
- await recomputeParentsCooperatively(parents, adapter, recomputeProgress, {
2382
+ const progressMode = recomputeProgress ? options.progressMode ?? "freeze-when-no-contribution" : "skip";
2383
+ await recomputeParentsCooperatively(parents, adapter, progressMode, {
2260
2384
  recomputeRealWork: options.recomputeRealWork ?? false
2261
2385
  });
2262
2386
  }
2263
- function recomputeParentBoundsSync(adapter, dirtyIds, recomputeProgress = true) {
2264
- const parents = findAncestorsOfDirty(adapter, dirtyIds);
2265
- for (const parentId of parents) {
2266
- recomputeParentFromChildren(parentId, adapter, recomputeProgress);
2267
- }
2268
- }
2269
2387
  function recomputeSingleParentDisplayBounds(parentId, adapter) {
2270
- recomputeParentFromChildren(parentId, adapter, false);
2388
+ recomputeParentFromChildren(parentId, adapter, "skip");
2271
2389
  }
2272
- async function recomputeParentsCooperatively(parents, adapter, recomputeProgress = true, options = {
2390
+ async function recomputeParentsCooperatively(parents, adapter, progressMode, options = {
2273
2391
  recomputeRealWork: false
2274
2392
  }) {
2275
2393
  const YIELD_EVERY_N_PARENTS = 200;
@@ -2278,7 +2396,7 @@ async function recomputeParentsCooperatively(parents, adapter, recomputeProgress
2278
2396
  recomputeParentFromChildren(
2279
2397
  parentId,
2280
2398
  adapter,
2281
- recomputeProgress,
2399
+ progressMode,
2282
2400
  options.recomputeRealWork
2283
2401
  );
2284
2402
  processed += 1;
@@ -2328,7 +2446,7 @@ function aggregateChildren(childrenIds, adapter, includeRealWork) {
2328
2446
  }
2329
2447
  return { minStart, maxEnd, realWorkSum, weightedChildren };
2330
2448
  }
2331
- function recomputeParentFromChildren(parentId, adapter, recomputeProgress = true, recomputeRealWork = false) {
2449
+ function recomputeParentFromChildren(parentId, adapter, progressMode = "freeze-when-no-contribution", recomputeRealWork = false) {
2332
2450
  const parent = adapter.getActivity(parentId);
2333
2451
  if (!parent) return;
2334
2452
  const childrenIds = adapter.getChildren(parentId);
@@ -2346,11 +2464,12 @@ function recomputeParentFromChildren(parentId, adapter, recomputeProgress = true
2346
2464
  const progressRollup = computeWeightedProgressRollup(
2347
2465
  aggregate.weightedChildren
2348
2466
  );
2349
- if (recomputeProgress && progressRollup !== null) {
2467
+ const shouldWriteProgress = progressMode !== "skip" && (progressRollup !== null || progressMode === "zero-when-no-contribution");
2468
+ if (shouldWriteProgress) {
2350
2469
  adapter.setActivityField(
2351
2470
  parentId,
2352
2471
  "progress",
2353
- roundProgressPerLevel(progressRollup)
2472
+ roundProgressPerLevel(progressRollup ?? 0)
2354
2473
  );
2355
2474
  }
2356
2475
  if (recomputeRealWork) {
@@ -2412,7 +2531,7 @@ function runRecordLastStartDate(activityId, adapter) {
2412
2531
  const activity = adapter.getActivity(activityId);
2413
2532
  if (activity) adapter.setLastStartDate(activityId, activity.startDate);
2414
2533
  }
2415
- function runPostProcessorsOnAdapter(activityId, processorNames, adapter) {
2534
+ function runPostProcessorsOnAdapter(activityId, processorNames, adapter, preEditActivity) {
2416
2535
  for (const name of processorNames) {
2417
2536
  const fn = name in POST_PROCESSORS ? POST_PROCESSORS[name] : void 0;
2418
2537
  if (!fn) {
@@ -2421,7 +2540,7 @@ function runPostProcessorsOnAdapter(activityId, processorNames, adapter) {
2421
2540
  );
2422
2541
  continue;
2423
2542
  }
2424
- fn(activityId, adapter);
2543
+ fn(activityId, adapter, preEditActivity ?? null);
2425
2544
  }
2426
2545
  }
2427
2546
  function runNoOp(name) {
@@ -2694,10 +2813,7 @@ var durationPipeline = {
2694
2813
  );
2695
2814
  return {
2696
2815
  ...fieldChanges(extraFields, {
2697
- // MAIN treats an ALAP duration edit as a resize from Start: Start is
2698
- // anchored and Finish moves. Running the ALAP pass would instead
2699
- // preserve Finish and pull Start, which is not the observed UI flow.
2700
- autoSchedule: activity.constraintType !== "alap",
2816
+ autoSchedule: true,
2701
2817
  postProcessors: [POST_PROCESSOR.RECORD_LAST_START_DATE]
2702
2818
  }),
2703
2819
  ...constraintCheck.violated ? {
@@ -2859,16 +2975,22 @@ function buildCanonicalRealWorkOverlay(activity, progress, hierarchy) {
2859
2975
  realWorkByActivity.set(activity.id, selfRealWork);
2860
2976
  return realWorkByActivity;
2861
2977
  }
2978
+ function summariesOverSummariesDeepestFirst(activity, hierarchy) {
2979
+ const subtreeIds = [activity.id, ...hierarchy.getDescendantIds(activity.id)];
2980
+ const hasSummaryChild = (parentId) => hierarchy.getChildProgressInfo(parentId).some((child) => hierarchy.hasChildren(child.id));
2981
+ const depthOf3 = (activityId) => hierarchy.getAncestorIds(activityId).length;
2982
+ return subtreeIds.filter((candidateId) => hasSummaryChild(String(candidateId))).map(String).sort((first, second) => depthOf3(second) - depthOf3(first));
2983
+ }
2862
2984
  function buildAncestorRollupCascade(activity, newValue, hierarchy) {
2863
2985
  const overlay = /* @__PURE__ */ new Map();
2864
2986
  overlay.set(activity.id, newValue);
2865
2987
  for (const descendantId of hierarchy.getDescendantIds(activity.id)) {
2866
2988
  overlay.set(descendantId, newValue);
2867
2989
  }
2868
- const chain = [];
2869
- for (const ancestorId of hierarchy.getAncestorIds(activity.id)) {
2870
- chain.push(ancestorId);
2871
- }
2990
+ const chain = [
2991
+ ...summariesOverSummariesDeepestFirst(activity, hierarchy),
2992
+ ...hierarchy.getAncestorIds(activity.id)
2993
+ ];
2872
2994
  const mutations = [];
2873
2995
  for (const id of chain) {
2874
2996
  const rollup = computeWeightedProgress(id, overlay, hierarchy);
@@ -3265,6 +3387,8 @@ function checkIsWorkingDay(newDate, activity, ctx) {
3265
3387
  return null;
3266
3388
  }
3267
3389
  function detectEndDateConstraintWarning(activity, newEndDate) {
3390
+ const cannotViolateStartPin = effectiveConstraintType(activity) === CONSTRAINT_TYPE2.MSO;
3391
+ if (cannotViolateStartPin) return void 0;
3268
3392
  const projectedDate = pickProjectedDateForConstraint(
3269
3393
  effectiveConstraintType(activity),
3270
3394
  activity.startDate,
@@ -3285,7 +3409,8 @@ function detectEndDateConstraintWarning(activity, newEndDate) {
3285
3409
  function buildEndDateExtraFields(activity, newEndDate, ctx, violatesConstraint = false) {
3286
3410
  const fields = {};
3287
3411
  const isMilestone = isMilestoneType(activity.type);
3288
- if (isMilestone && !violatesConstraint) {
3412
+ const seedsMilestoneDate = isMilestone && !violatesConstraint && hasDatelessConstraintType(activity);
3413
+ if (seedsMilestoneDate) {
3289
3414
  Object.assign(fields, setConstraintDate(activity.startDate));
3290
3415
  } else if (violatesConstraint || hasDatelessConstraintType(activity)) {
3291
3416
  Object.assign(
@@ -3295,10 +3420,8 @@ function buildEndDateExtraFields(activity, newEndDate, ctx, violatesConstraint =
3295
3420
  );
3296
3421
  }
3297
3422
  const newDuration = calculateNewDuration(activity, newEndDate, ctx);
3298
- if (!isMilestone) {
3299
- const flippedType = typeForNewDuration(activity.durationHours, newDuration);
3300
- if (flippedType) fields.type = flippedType;
3301
- }
3423
+ const flippedType = typeForNewDuration(activity.durationHours, newDuration);
3424
+ if (flippedType) fields.type = flippedType;
3302
3425
  Object.assign(fields, setDuration(newDuration));
3303
3426
  fields.endDate = newEndDate;
3304
3427
  return fields;
@@ -3715,6 +3838,49 @@ function setLinkFieldDynamic(state, linkId, field, value) {
3715
3838
  state.setLinkFieldDynamic(linkId, field, value);
3716
3839
  }
3717
3840
 
3841
+ // src/internal/state/clone-core-activity.ts
3842
+ function cloneCoreActivity(activity) {
3843
+ return {
3844
+ ...activity,
3845
+ startDate: new Date(activity.startDate),
3846
+ endDate: new Date(activity.endDate),
3847
+ constraintDate: cloneNullableDate(activity.constraintDate),
3848
+ dateOrigin: cloneNullableDate(activity.dateOrigin),
3849
+ newActivityIds: [...activity.newActivityIds],
3850
+ pendingRequestIds: [...activity.pendingRequestIds],
3851
+ responsableIds: [...activity.responsableIds],
3852
+ tagIds: [...activity.tagIds],
3853
+ baselinePoints: activity.baselinePoints.map((point) => ({
3854
+ ...point,
3855
+ startDate: cloneNullableDate(point.startDate),
3856
+ endDate: cloneNullableDate(point.endDate)
3857
+ })),
3858
+ baselineSnapshot: cloneBaselineSnapshot(activity.baselineSnapshot),
3859
+ criticalPath: cloneCriticalPath(activity.criticalPath)
3860
+ };
3861
+ }
3862
+ function cloneNullableDate(value) {
3863
+ return value === null ? null : new Date(value);
3864
+ }
3865
+ function cloneBaselineSnapshot(value) {
3866
+ if (value === null) return null;
3867
+ return {
3868
+ ...value,
3869
+ startDate: cloneNullableDate(value.startDate),
3870
+ endDate: cloneNullableDate(value.endDate)
3871
+ };
3872
+ }
3873
+ function cloneCriticalPath(value) {
3874
+ if (value === null) return null;
3875
+ return {
3876
+ ...value,
3877
+ earlyStart: cloneNullableDate(value.earlyStart),
3878
+ earlyFinish: cloneNullableDate(value.earlyFinish),
3879
+ lateStart: cloneNullableDate(value.lateStart),
3880
+ lateFinish: cloneNullableDate(value.lateFinish)
3881
+ };
3882
+ }
3883
+
3718
3884
  // src/dispatch/shared/snapshots.ts
3719
3885
  function collectTouchedIds(primary, changes) {
3720
3886
  const ids = /* @__PURE__ */ new Set([String(primary)]);
@@ -3732,13 +3898,7 @@ function snapshotActivities(adapter, ids) {
3732
3898
  return out;
3733
3899
  }
3734
3900
  function structuredCloneActivity(activity) {
3735
- return {
3736
- ...activity,
3737
- startDate: new Date(activity.startDate),
3738
- endDate: new Date(activity.endDate),
3739
- constraintDate: activity.constraintDate ? new Date(activity.constraintDate) : null,
3740
- dateOrigin: activity.dateOrigin ? new Date(activity.dateOrigin) : null
3741
- };
3901
+ return cloneCoreActivity(activity);
3742
3902
  }
3743
3903
  function applyFieldChanges(adapter, activityId, changes) {
3744
3904
  applyCanonicalPatch(adapter, activityId, changes.patch);
@@ -3766,6 +3926,7 @@ function buildActivityChanges(adapter, before, touched) {
3766
3926
  }
3767
3927
  const beforeSnap = before.get(id);
3768
3928
  const diff = diffActivity(beforeSnap, afterSnap);
3929
+ if (beforeSnap && Object.keys(diff).length === 0) continue;
3769
3930
  if (!beforeSnap) {
3770
3931
  out.push({
3771
3932
  id,
@@ -3790,12 +3951,33 @@ function diffActivity(before, after) {
3790
3951
  const afterRec = after;
3791
3952
  const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRec), ...Object.keys(afterRec)]);
3792
3953
  for (const k of keys) {
3793
- if (!shallowEqual(beforeRec[k], afterRec[k])) {
3954
+ if (Object.hasOwn(beforeRec, k) !== Object.hasOwn(afterRec, k) || !fieldValueEqual(beforeRec[k], afterRec[k])) {
3794
3955
  fields[k] = { before: beforeRec[k], after: afterRec[k] };
3795
3956
  }
3796
3957
  }
3797
3958
  return fields;
3798
3959
  }
3960
+ function fieldValueEqual(left, right) {
3961
+ if (Object.is(left, right)) return true;
3962
+ if (left instanceof Date && right instanceof Date) {
3963
+ return left.getTime() === right.getTime();
3964
+ }
3965
+ if (left == null || right == null) return false;
3966
+ if (Array.isArray(left) || Array.isArray(right)) {
3967
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
3968
+ if (left.length !== right.length) return false;
3969
+ return left.every((value, index) => fieldValueEqual(value, right[index]));
3970
+ }
3971
+ if (typeof left !== "object" || typeof right !== "object") return false;
3972
+ const leftRecord = left;
3973
+ const rightRecord = right;
3974
+ const leftKeys = Object.keys(leftRecord);
3975
+ const rightKeys = Object.keys(rightRecord);
3976
+ if (leftKeys.length !== rightKeys.length) return false;
3977
+ return leftKeys.every(
3978
+ (key) => Object.hasOwn(rightRecord, key) && fieldValueEqual(leftRecord[key], rightRecord[key])
3979
+ );
3980
+ }
3799
3981
  function collectDirtyForInlineEdit(triggerId, touchedIds, scheduledIds) {
3800
3982
  const dirty = /* @__PURE__ */ new Set();
3801
3983
  dirty.add(triggerId);
@@ -3821,21 +4003,6 @@ function foldCorrelativeShifts(adapter, shifts, excludeIds, beforeSnap, touched)
3821
4003
  beforeSnap.set(shift.activityId, beforeImage);
3822
4004
  }
3823
4005
  }
3824
- function shallowEqual(a, b) {
3825
- if (a === b) return true;
3826
- if (a instanceof Date && b instanceof Date)
3827
- return a.getTime() === b.getTime();
3828
- if (a == null || b == null) return false;
3829
- if (typeof a !== typeof b) return false;
3830
- if (Array.isArray(a) && Array.isArray(b)) {
3831
- if (a.length !== b.length) return false;
3832
- for (let i = 0; i < a.length; i++) {
3833
- if (!shallowEqual(a[i], b[i])) return false;
3834
- }
3835
- return true;
3836
- }
3837
- return false;
3838
- }
3839
4006
 
3840
4007
  // src/shared/backend-date.ts
3841
4008
  var BACKEND_DATE_FORMAT_REGEX = /^(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{2})$/;
@@ -3919,6 +4086,77 @@ function computeExpectedProgressLive(rootIds, now, state) {
3919
4086
  for (const activityId of rootIds) visit(activityId);
3920
4087
  return changed;
3921
4088
  }
4089
+ function applyExpectedProgressLive(state, now) {
4090
+ const rootIds = [];
4091
+ state.forEachActivity((activity, activityId) => {
4092
+ if (activity.parentId === null) rootIds.push(activityId);
4093
+ });
4094
+ computeExpectedProgressLive(rootIds, now, state);
4095
+ }
4096
+
4097
+ // src/propagations/shared/get-active-baseline.ts
4098
+ function getActiveBaseline(activity) {
4099
+ const points = activity?.baselinePoints;
4100
+ if (!Array.isArray(points) || points.length === 0) return null;
4101
+ return points.find((point) => point.isActiveVersion) ?? null;
4102
+ }
4103
+
4104
+ // src/columns/expected-progress-base/compute.ts
4105
+ var isProject2 = (a) => a.type === "project";
4106
+ var activeBaselineOf = (a) => getActiveBaseline(a);
4107
+ var leafExpected = (a, now, adapter, defaultBaseCalendarId) => {
4108
+ const base = activeBaselineOf(a);
4109
+ if (!base) return 0;
4110
+ const cal = (base.baseCalendarId != null ? adapter.getBaseCalendar(`${base.baseCalendarId}-base`) : null) ?? (defaultBaseCalendarId ? adapter.getBaseCalendar(defaultBaseCalendarId) : null);
4111
+ if (!cal) return 0;
4112
+ if (!base.startDate || !base.endDate) return 0;
4113
+ return expectedProgressFromBaseline(base.startDate, base.endDate, now, cal);
4114
+ };
4115
+ function computeExpectedProgress(rootIds, now, adapter, defaultBaseCalendarId = null) {
4116
+ const changed = [];
4117
+ const writeIfChanged = (id, value) => {
4118
+ const a = adapter.getActivity(id);
4119
+ if (!a) return;
4120
+ if (a.expectedProgressBaseline !== value) {
4121
+ adapter.setActivityField(id, "expectedProgressBaseline", value);
4122
+ changed.push(id);
4123
+ }
4124
+ };
4125
+ const visit = (id) => {
4126
+ const a = adapter.getActivity(id);
4127
+ if (!a) return 0;
4128
+ if (!isProject2(a)) {
4129
+ const v2 = leafExpected(a, now, adapter, defaultBaseCalendarId);
4130
+ writeIfChanged(id, v2);
4131
+ return v2;
4132
+ }
4133
+ let acc = 0;
4134
+ for (const childId of adapter.getChildrenIds(id)) {
4135
+ const childBase = visit(childId);
4136
+ const child = adapter.getActivity(childId);
4137
+ const weight = typeof child?.ponderator === "number" ? child.ponderator : 0;
4138
+ acc += childBase * weight;
4139
+ }
4140
+ const v = acc / 100;
4141
+ writeIfChanged(id, v);
4142
+ return v;
4143
+ };
4144
+ for (const id of rootIds) visit(id);
4145
+ return changed;
4146
+ }
4147
+ function runExpectedProgressBase(state, now, defaultBaseCalendarId = null) {
4148
+ const adapter = {
4149
+ getChildrenIds: (parentId) => state.getChildren(String(parentId)).map((childId) => String(childId)),
4150
+ getActivity: (id) => state.getActivity(id),
4151
+ setActivityField: (id, field, value) => setActivityFieldDynamic(state, id, field, value),
4152
+ getBaseCalendar: (engineId) => state.calendarReader.getCalendar(engineId)
4153
+ };
4154
+ const rootIds = [];
4155
+ state.forEachActivity((activity, id) => {
4156
+ if (activity.parentId === null) rootIds.push(id);
4157
+ });
4158
+ return computeExpectedProgress(rootIds, now, adapter, defaultBaseCalendarId);
4159
+ }
3922
4160
 
3923
4161
  // src/columns/status/compute.ts
3924
4162
  var roundToTwoDecimals = (value) => Math.round(value * 100) / 100;
@@ -3954,552 +4192,248 @@ function applyStatusPass(state, criteria) {
3954
4192
  return changed;
3955
4193
  }
3956
4194
 
3957
- // src/internal/hierarchy/visual-order.ts
3958
- var NOT_SET_SORT_VALUE = Number.MAX_SAFE_INTEGER;
3959
- function getChildrenInVisualOrder(parentId, adapter) {
3960
- const children = collectChildrenSnapshots(parentId, adapter);
3961
- children.sort(byCorrelativeIdThenId);
3962
- return children.map((a) => String(a.id));
3963
- }
3964
- function iterateInVisualOrder(adapter) {
3965
- const result = [];
3966
- const visit = (activity) => {
3967
- result.push(activity);
3968
- const childIds = getChildrenInVisualOrder(String(activity.id), adapter);
3969
- for (const childId of childIds) {
3970
- const child = adapter.getActivity(childId);
3971
- if (child) visit(child);
3972
- }
4195
+ // src/critical-path/legacy/shims/moment.js
4196
+ function moment(input) {
4197
+ const d = input instanceof Date ? new Date(input.getTime()) : input && typeof input === "object" && typeof input.valueOf === "function" ? new Date(input.valueOf()) : new Date(input);
4198
+ const accessor = (getUTC, setUTC) => (value) => {
4199
+ if (value === void 0) return d[getUTC]();
4200
+ d[setUTC](value);
4201
+ return api;
3973
4202
  };
3974
- const rootIds = getChildrenInVisualOrder(ROOT_PARENT_ID, adapter);
3975
- for (const rootId of rootIds) {
3976
- const root = adapter.getActivity(rootId);
3977
- if (root) visit(root);
3978
- }
3979
- return result;
3980
- }
3981
- function findPreviousNonSelectedSibling(taskId, selected, adapter) {
3982
- const activity = adapter.getActivity(taskId);
3983
- if (!activity) return null;
3984
- const parentKey = parentKeyOf(activity);
3985
- const siblings = getChildrenInVisualOrder(parentKey, adapter);
3986
- const idx = siblings.findIndex((id) => String(id) === String(taskId));
3987
- if (idx <= 0) return null;
3988
- for (let i = idx - 1; i >= 0; i--) {
3989
- const candidateId = siblings[i];
3990
- if (candidateId === void 0) continue;
3991
- if (!setHas(selected, candidateId)) return candidateId;
3992
- }
3993
- return null;
3994
- }
3995
- function visualIndexInParent(taskId, adapter) {
3996
- const activity = adapter.getActivity(taskId);
3997
- if (!activity) return -1;
3998
- const parentKey = parentKeyOf(activity);
3999
- const siblings = getChildrenInVisualOrder(parentKey, adapter);
4000
- return siblings.findIndex((id) => String(id) === String(taskId));
4203
+ const api = {
4204
+ __isMomentShim: true,
4205
+ clone: () => moment(d),
4206
+ hours: accessor("getUTCHours", "setUTCHours"),
4207
+ minutes: accessor("getUTCMinutes", "setUTCMinutes"),
4208
+ seconds: accessor("getUTCSeconds", "setUTCSeconds"),
4209
+ milliseconds: accessor("getUTCMilliseconds", "setUTCMilliseconds"),
4210
+ valueOf: () => d.getTime(),
4211
+ toDate: () => new Date(d.getTime()),
4212
+ getTime: () => d.getTime()
4213
+ };
4214
+ return api;
4001
4215
  }
4002
- function collectChildrenSnapshots(parentId, adapter) {
4003
- const key = String(parentId);
4004
- if (key === "0") {
4005
- return adapter.getAllActivities().filter((a) => a.parentId === null);
4006
- }
4007
- const childIds = adapter.getChildren(parentId);
4008
- const out = [];
4009
- for (const id of childIds) {
4010
- const snap = adapter.getActivity(id);
4011
- if (snap) out.push(snap);
4216
+ moment.isMoment = (x) => Boolean(x) && typeof x === "object" && x.__isMomentShim === true;
4217
+ var moment_default = moment;
4218
+
4219
+ // src/critical-path/legacy/helpers/isValidDate.js
4220
+ function isDate(obj) {
4221
+ if (obj && obj.constructor === Date) {
4222
+ return Boolean(obj.getFullYear && obj.getMonth && obj.getDate);
4012
4223
  }
4013
- return out;
4014
- }
4015
- function byCorrelativeIdThenId(a, b) {
4016
- const ac = correlativeIdOf(a);
4017
- const bc = correlativeIdOf(b);
4018
- if (ac !== bc) return ac - bc;
4019
- return String(a.id).localeCompare(String(b.id));
4020
- }
4021
- function correlativeIdOf(activity) {
4022
- const raw = activity.correlativeId;
4023
- const n = typeof raw === "number" ? raw : Number(raw);
4024
- return Number.isFinite(n) ? n : NOT_SET_SORT_VALUE;
4025
- }
4026
- function parentKeyOf(activity) {
4027
- const parentId = activity.parentId;
4028
- if (parentId === null) return "0";
4029
- return parentId;
4224
+ return false;
4030
4225
  }
4031
- function setHas(set, id) {
4032
- if (set.has(id)) return true;
4033
- return set.has(String(id));
4226
+ function isValidDate2(obj) {
4227
+ return isDate(obj) && !isNaN(obj.getTime());
4034
4228
  }
4035
4229
 
4036
- // src/internal/hierarchy/recompute-correlative-ids.ts
4037
- function recomputeCorrelativeIds(adapter) {
4038
- adapter.invalidateVisualOrderIds?.();
4039
- const roots = [];
4040
- adapter.forEachActivity((activity) => {
4041
- if (isRootParent(activity.parentId)) roots.push(activity);
4042
- });
4043
- roots.sort(byCorrelativeId);
4044
- const stack = [];
4045
- pushInReverseOrder(stack, roots);
4046
- const shifts = [];
4047
- let counter = 0;
4048
- while (stack.length > 0) {
4049
- const activity = stack.pop();
4050
- if (activity.correlativeId !== counter) {
4051
- const before = typeof activity.correlativeId === "number" ? activity.correlativeId : void 0;
4052
- shifts.push({ activityId: String(activity.id), before });
4053
- if (before !== void 0)
4054
- adapter.noteCorrelativeBefore?.(String(activity.id), before);
4055
- setCorrelativeId(activity, counter);
4056
- }
4057
- counter += 1;
4058
- const children = collectChildrenSnapshots(String(activity.id), adapter);
4059
- children.sort(byCorrelativeId);
4060
- pushInReverseOrder(stack, children);
4230
+ // src/critical-path/legacy/helpers/addDurationToDate.js
4231
+ function addDurationToDate(activityCalendar, date2, duration, activity) {
4232
+ if (!isValidDate2(new Date(date2))) {
4233
+ throw new Error(
4234
+ `Invalid date argument for calculateEndDate method. Date: ${date2}, Activity: ${JSON.stringify(activity)}`
4235
+ );
4061
4236
  }
4062
- return shifts;
4237
+ const dateToCalculate = date2;
4238
+ return activityCalendar.calculateEndDate({
4239
+ start_date: new Date(dateToCalculate),
4240
+ duration,
4241
+ unit: "hour",
4242
+ task: activity
4243
+ });
4063
4244
  }
4064
- function pushInReverseOrder(stack, activities) {
4065
- for (let i = activities.length - 1; i >= 0; i -= 1) {
4066
- stack.push(activities[i]);
4245
+
4246
+ // src/critical-path/legacy/helpers/getNextWorkingHour.js
4247
+ function getNextWorkingHour(activityParams) {
4248
+ if (!isValidDate2(new Date(activityParams.dateBaseToCalculate))) {
4249
+ throw new Error("Invalid date argument for getClosestWorkTime method");
4067
4250
  }
4068
- }
4069
- function getCorrelativeId(activity) {
4070
- const raw = activity.correlativeId;
4071
- const n = typeof raw === "number" ? raw : Number(raw);
4072
- return Number.isFinite(n) ? n : NOT_SET_SORT_VALUE;
4073
- }
4074
- var byCorrelativeId = (a, b) => getCorrelativeId(a) - getCorrelativeId(b);
4075
- function setCorrelativeId(activity, value) {
4076
- activity.correlativeId = value;
4251
+ const { dateBaseToCalculate, activityCalendar, direction, activity } = activityParams;
4252
+ const newDate = activityCalendar.$gantt.getClosestWorkTime({
4253
+ date: new Date(dateBaseToCalculate),
4254
+ dir: direction,
4255
+ unit: "hour",
4256
+ task: activity
4257
+ });
4258
+ return newDate;
4077
4259
  }
4078
4260
 
4079
- // src/propagations/upward/recompute-real-work.ts
4080
- function recomputeCanonicalRealWork(state) {
4081
- const rootIds = [];
4082
- const changedIds = [];
4083
- state.forEachActivity((activity, activityId) => {
4084
- if (activity.parentId === null) rootIds.push(String(activityId));
4085
- });
4086
- const visit = (activityId) => {
4087
- const activity = state.getActivity(activityId);
4088
- if (!activity) return 0;
4089
- const childIds = [...state.getChildren(activityId)];
4090
- const realWork = childIds.length === 0 ? computeLeafRealWork(activity.workHours, activity.progress) : childIds.reduce(
4091
- (total, childId) => total + visit(String(childId)),
4092
- 0
4093
- );
4094
- state.setActivityField(
4095
- activityId,
4096
- ACTIVITY_PROPERTY.REAL_WORK_HOURS,
4097
- realWork
4098
- );
4099
- changedIds.push(activityId);
4100
- return realWork;
4261
+ // src/critical-path/legacy/helpers/filters.js
4262
+ var byFirstLevel = (activity) => {
4263
+ if (!activity) return false;
4264
+ return activity["$level"] === 1;
4265
+ };
4266
+ var filterByParentType = (activity) => activity.type === "project";
4267
+ var avoidSubproject = (activity) => activity["$level"] !== 0;
4268
+ var byTaskType = (activity) => activity.type === "task";
4269
+ var byTaskTypeAndMilestone = (activity) => activity.type === "task" || activity.type === "milestone";
4270
+ var filters_default = {
4271
+ byFirstLevel,
4272
+ filterByParentType,
4273
+ avoidSubproject,
4274
+ byTaskType,
4275
+ byTaskTypeAndMilestone
4276
+ };
4277
+
4278
+ // src/critical-path/legacy/constants/constraints.js
4279
+ var CONSTRAINT_TYPES = Object.freeze({
4280
+ MFO: "mfo",
4281
+ MSO: "mso",
4282
+ FNLT: "fnlt",
4283
+ FNET: "fnet",
4284
+ SNET: "snet",
4285
+ SNLT: "snlt",
4286
+ ALAP: "alap",
4287
+ ASAP: "asap"
4288
+ });
4289
+
4290
+ // src/critical-path/legacy/constants/linkTypes.js
4291
+ var LINK_TYPES = Object.freeze({
4292
+ FS: "fs",
4293
+ SS: "ss",
4294
+ FF: "ff",
4295
+ SF: "sf"
4296
+ });
4297
+
4298
+ // src/critical-path/legacy/constants/linkByCode.js
4299
+ function getLinkNameByCode(code) {
4300
+ const linkMap = {
4301
+ 0: "fs",
4302
+ 1: "ss",
4303
+ 2: "ff",
4304
+ 3: "sf"
4101
4305
  };
4102
- for (const rootId of rootIds) visit(rootId);
4103
- return changedIds;
4306
+ return linkMap[Number(code)] || "sf";
4104
4307
  }
4105
4308
 
4106
- // src/init/initial-passes.ts
4107
- async function runInitialPasses(deps) {
4108
- const { state, sector, scheduler, skipAutoSchedule, now } = deps;
4109
- recomputeCorrelativeIds(state);
4110
- if (sector.updateDurationForPrimaveraEndDate) {
4111
- recomputeDurationsFromDates(state);
4112
- }
4113
- normalizeLoadedConstraintDates(state);
4114
- recomputeCanonicalRealWork(state);
4115
- if (skipAutoSchedule) {
4116
- if (now) {
4117
- applyExpectedProgressLive(state, now);
4118
- applyStatusPass(state, sector.statusCriteria);
4309
+ // src/critical-path/legacy/base/identifiers/secondLevelActivities.js
4310
+ function identifySecondLevelActivities(params) {
4311
+ const { gantt, pendingParentsWithNoLinks, calculations, linkProperty } = params;
4312
+ try {
4313
+ const subprojectsWithNoLinks = getSubprojectsWithNoLinks({
4314
+ gantt,
4315
+ pendingParentsWithNoLinks
4316
+ });
4317
+ if (subprojectsWithNoLinks.length === 0) {
4318
+ return /* @__PURE__ */ new Set();
4119
4319
  }
4120
- return;
4121
- }
4122
- const result = await scheduler.schedule({});
4123
- applyResults(state, result);
4124
- normalizeMilestoneConstraintDatesForDisplay(state);
4125
- recomputeSkippedLeafEnds(state);
4126
- await updateParentBoundsFromChildren(state, void 0, false);
4127
- if (now) {
4128
- applyExpectedProgressLive(state, now);
4129
- applyStatusPass(state, sector.statusCriteria);
4320
+ const allChildrenFromParents = getAllChildrenFromParents({
4321
+ gantt,
4322
+ subprojectsWithNoLinks
4323
+ });
4324
+ const childrenThatCanBeCalculated = filterChildrenThatCanBeCalculated({
4325
+ gantt,
4326
+ childrenIds: allChildrenFromParents,
4327
+ linkProperty,
4328
+ getArrayOfLinkedActivities,
4329
+ calculations
4330
+ });
4331
+ return new Set(childrenThatCanBeCalculated.map((activity) => activity.id));
4332
+ } catch (e) {
4333
+ throw e;
4130
4334
  }
4131
4335
  }
4132
- function applyExpectedProgressLive(state, now) {
4133
- const rootIds = [];
4134
- state.forEachActivity((activity, activityId) => {
4135
- if (activity.parentId === null) rootIds.push(activityId);
4136
- });
4137
- computeExpectedProgressLive(rootIds, now, state);
4336
+ function getSubprojectsWithNoLinks(params) {
4337
+ const { gantt, pendingParentsWithNoLinks } = params;
4338
+ return gantt.getTaskByTime().filter(filters_default.filterByParentType).filter((activity) => pendingParentsWithNoLinks.has(activity.id)).map((activity) => activity.id);
4138
4339
  }
4139
- function recomputeSkippedLeafEnds(state) {
4140
- state.forEachActivity((activity) => {
4141
- if (activity.autoScheduling) return;
4142
- if (!activity.startDate || activity.durationHours == null) return;
4143
- if (state.getChildren(activity.id).length > 0) return;
4144
- activity.endDate = state.calculateEndDate({
4145
- startDate: activity.startDate,
4146
- durationHours: activity.durationHours,
4147
- task: activity
4340
+ function getAllChildrenFromParents(params) {
4341
+ const { gantt, subprojectsWithNoLinks } = params;
4342
+ const allChildrenSet = /* @__PURE__ */ new Set();
4343
+ for (const subprojectId of subprojectsWithNoLinks) {
4344
+ const childrenIds = gantt.getChildren(subprojectId);
4345
+ const validChildren = childrenIds.filter((childId) => {
4346
+ const childActivity = gantt.getTask(childId);
4347
+ const isTaskOrMilestone = filters_default.byTaskTypeAndMilestone(childActivity);
4348
+ const hasProgressLessThan100 = Number(childActivity.progress) < 100;
4349
+ return isTaskOrMilestone && hasProgressLessThan100;
4148
4350
  });
4149
- });
4351
+ validChildren.forEach((childId) => allChildrenSet.add(childId));
4352
+ }
4353
+ return [...allChildrenSet];
4150
4354
  }
4151
- function recomputeDurationsFromDates(state) {
4152
- state.forEachActivity((activity) => {
4153
- if (!activity.startDate || !activity.endDate) return;
4154
- activity.durationHours = state.calculateDuration({
4155
- startDate: activity.startDate,
4156
- endDate: activity.endDate,
4157
- task: activity
4355
+ function filterChildrenThatCanBeCalculated(params) {
4356
+ const {
4357
+ gantt,
4358
+ childrenIds,
4359
+ linkProperty,
4360
+ getArrayOfLinkedActivities: getArrayOfLinkedActivities3,
4361
+ calculations
4362
+ } = params;
4363
+ const calculableChildren = [];
4364
+ for (const childId of childrenIds) {
4365
+ const activity = gantt.getTask(childId);
4366
+ const links = activity[linkProperty] || [];
4367
+ const linkedActivities = getArrayOfLinkedActivities3({
4368
+ gantt,
4369
+ links,
4370
+ linkDirection: linkProperty === "$source" ? "target" : "source"
4158
4371
  });
4159
- });
4160
- }
4161
- function normalizeLoadedConstraintDates(state) {
4162
- state.forEachActivity((activity) => {
4163
- const type = activity.constraintType;
4164
- if (!type || !activity.constraintDate) return;
4165
- const constraintDate = activity.constraintDate;
4166
- if (activity.type === "milestone" && (constraintDate.getUTCHours() !== 0 || constraintDate.getUTCMinutes() !== 0)) {
4167
- return;
4168
- }
4169
- const calendar = state.calendarReader.getCalendar(
4170
- String(activity.calendarId)
4171
- );
4172
- if (!calendar) return;
4173
- activity.constraintDate = normalizeConstraintDate(
4174
- activity.constraintDate,
4175
- calendar,
4176
- type
4177
- );
4178
- });
4179
- }
4180
- function normalizeMilestoneConstraintDatesForDisplay(state) {
4181
- state.forEachActivity((activity) => {
4182
- if (activity.type !== "milestone") return;
4183
- const type = activity.constraintType;
4184
- if (!type || !activity.constraintDate) return;
4185
- const calendar = state.calendarReader.getCalendar(
4186
- String(activity.calendarId)
4187
- );
4188
- if (!calendar) return;
4189
- activity.constraintDate = normalizeConstraintDate(
4190
- activity.constraintDate,
4191
- calendar,
4192
- type
4372
+ const isLinkedWithCalculatedOrSibling = linkedActivities.every(
4373
+ (linkedActivityId) => calculations.has(linkedActivityId)
4193
4374
  );
4194
- });
4375
+ if (isLinkedWithCalculatedOrSibling) {
4376
+ calculableChildren.push(activity);
4377
+ }
4378
+ }
4379
+ return calculableChildren;
4195
4380
  }
4196
-
4197
- // src/propagations/shared/get-active-baseline.ts
4198
- function getActiveBaseline(activity) {
4199
- const points = activity?.baselinePoints;
4200
- if (!Array.isArray(points) || points.length === 0) return null;
4201
- return points.find((point) => point.isActiveVersion) ?? null;
4381
+ function getArrayOfLinkedActivities(params) {
4382
+ const { gantt, links, linkDirection } = params;
4383
+ const linkedActivityIds = [];
4384
+ for (const linkId of links) {
4385
+ const link = gantt.getLink(linkId);
4386
+ if (!link) {
4387
+ continue;
4388
+ }
4389
+ const linkedActivityId = Number(link[linkDirection]);
4390
+ linkedActivityIds.push(linkedActivityId);
4391
+ }
4392
+ return linkedActivityIds;
4202
4393
  }
4203
4394
 
4204
- // src/columns/expected-progress-base/compute.ts
4205
- var isProject2 = (a) => a.type === "project";
4206
- var activeBaselineOf = (a) => getActiveBaseline(a);
4207
- var leafExpected = (a, now, adapter, defaultBaseCalendarId) => {
4208
- const base = activeBaselineOf(a);
4209
- if (!base) return 0;
4210
- const cal = (base.baseCalendarId != null ? adapter.getBaseCalendar(`${base.baseCalendarId}-base`) : null) ?? (defaultBaseCalendarId ? adapter.getBaseCalendar(defaultBaseCalendarId) : null);
4211
- if (!cal) return 0;
4212
- if (!base.startDate || !base.endDate) return 0;
4213
- return expectedProgressFromBaseline(base.startDate, base.endDate, now, cal);
4214
- };
4215
- function computeExpectedProgress(rootIds, now, adapter, defaultBaseCalendarId = null) {
4216
- const changed = [];
4217
- const writeIfChanged = (id, value) => {
4218
- const a = adapter.getActivity(id);
4219
- if (!a) return;
4220
- if (a.expectedProgressBaseline !== value) {
4221
- adapter.setActivityField(id, "expectedProgressBaseline", value);
4222
- changed.push(id);
4223
- }
4224
- };
4225
- const visit = (id) => {
4226
- const a = adapter.getActivity(id);
4227
- if (!a) return 0;
4228
- if (!isProject2(a)) {
4229
- const v2 = leafExpected(a, now, adapter, defaultBaseCalendarId);
4230
- writeIfChanged(id, v2);
4231
- return v2;
4395
+ // src/critical-path/legacy/base/identifiers/initialActivities.js
4396
+ function identifyInitialActivities({ gantt, linkProperty, direction }) {
4397
+ try {
4398
+ const chainStartActivities = getChainStartActivities(gantt, linkProperty);
4399
+ if (direction === "forward") {
4400
+ return {
4401
+ chainStartActivities,
4402
+ activitiesStartChainButChildrenOfLinkedParents: null
4403
+ };
4232
4404
  }
4233
- let acc = 0;
4234
- for (const childId of adapter.getChildrenIds(id)) {
4235
- const childBase = visit(childId);
4236
- const child = adapter.getActivity(childId);
4237
- const weight = typeof child?.ponderator === "number" ? child.ponderator : 0;
4238
- acc += childBase * weight;
4405
+ if (direction === "backward") {
4406
+ return processBackwardActivities(
4407
+ chainStartActivities,
4408
+ gantt,
4409
+ linkProperty
4410
+ );
4239
4411
  }
4240
- const v = acc / 100;
4241
- writeIfChanged(id, v);
4242
- return v;
4243
- };
4244
- for (const id of rootIds) visit(id);
4245
- return changed;
4246
- }
4247
- function runExpectedProgressBase(state, now, defaultBaseCalendarId = null) {
4248
- const adapter = {
4249
- getChildrenIds: (parentId) => state.getChildren(String(parentId)).map((childId) => String(childId)),
4250
- getActivity: (id) => state.getActivity(id),
4251
- setActivityField: (id, field, value) => setActivityFieldDynamic(state, id, field, value),
4252
- getBaseCalendar: (engineId) => state.calendarReader.getCalendar(engineId)
4253
- };
4254
- const rootIds = [];
4255
- state.forEachActivity((activity, id) => {
4256
- if (activity.parentId === null) rootIds.push(id);
4257
- });
4258
- return computeExpectedProgress(rootIds, now, adapter, defaultBaseCalendarId);
4259
- }
4260
-
4261
- // src/critical-path/legacy/shims/moment.js
4262
- function moment(input) {
4263
- const d = input instanceof Date ? new Date(input.getTime()) : input && typeof input === "object" && typeof input.valueOf === "function" ? new Date(input.valueOf()) : new Date(input);
4264
- const accessor = (getUTC, setUTC) => (value) => {
4265
- if (value === void 0) return d[getUTC]();
4266
- d[setUTC](value);
4267
- return api;
4268
- };
4269
- const api = {
4270
- __isMomentShim: true,
4271
- clone: () => moment(d),
4272
- hours: accessor("getUTCHours", "setUTCHours"),
4273
- minutes: accessor("getUTCMinutes", "setUTCMinutes"),
4274
- seconds: accessor("getUTCSeconds", "setUTCSeconds"),
4275
- milliseconds: accessor("getUTCMilliseconds", "setUTCMilliseconds"),
4276
- valueOf: () => d.getTime(),
4277
- toDate: () => new Date(d.getTime()),
4278
- getTime: () => d.getTime()
4279
- };
4280
- return api;
4281
- }
4282
- moment.isMoment = (x) => Boolean(x) && typeof x === "object" && x.__isMomentShim === true;
4283
- var moment_default = moment;
4284
-
4285
- // src/critical-path/legacy/helpers/isValidDate.js
4286
- function isDate(obj) {
4287
- if (obj && obj.constructor === Date) {
4288
- return Boolean(obj.getFullYear && obj.getMonth && obj.getDate);
4412
+ } catch (e) {
4413
+ console.log("Critical Path", e);
4414
+ throw e;
4289
4415
  }
4290
- return false;
4291
4416
  }
4292
- function isValidDate2(obj) {
4293
- return isDate(obj) && !isNaN(obj.getTime());
4417
+ function getChainStartActivities(gantt, linkProperty) {
4418
+ return gantt.getTaskByTime().filter(filters_default.avoidSubproject).filter(filters_default.byTaskTypeAndMilestone).filter((activity) => !activity[linkProperty].length);
4294
4419
  }
4295
-
4296
- // src/critical-path/legacy/helpers/addDurationToDate.js
4297
- function addDurationToDate(activityCalendar, date2, duration, activity) {
4298
- if (!isValidDate2(new Date(date2))) {
4299
- throw new Error(
4300
- `Invalid date argument for calculateEndDate method. Date: ${date2}, Activity: ${JSON.stringify(activity)}`
4301
- );
4302
- }
4303
- const dateToCalculate = date2;
4304
- return activityCalendar.calculateEndDate({
4305
- start_date: new Date(dateToCalculate),
4306
- duration,
4307
- unit: "hour",
4308
- task: activity
4420
+ function processBackwardActivities(chainStartActivities, gantt, linkProperty) {
4421
+ const withoutLinkedParents = [];
4422
+ const activitiesStartChainButChildrenOfLinkedParents = /* @__PURE__ */ new Set();
4423
+ chainStartActivities.forEach((activity) => {
4424
+ const parentId = Number(activity.parent);
4425
+ const parentActivity = gantt.getTask(parentId);
4426
+ if (!parentActivity) return;
4427
+ if (parentActivity[linkProperty].length > 0) {
4428
+ activitiesStartChainButChildrenOfLinkedParents.add(activity.id);
4429
+ } else if (parentActivity.constraint_type !== "fnlt") {
4430
+ withoutLinkedParents.push(activity);
4431
+ }
4309
4432
  });
4310
- }
4311
-
4312
- // src/critical-path/legacy/helpers/getNextWorkingHour.js
4313
- function getNextWorkingHour(activityParams) {
4314
- if (!isValidDate2(new Date(activityParams.dateBaseToCalculate))) {
4315
- throw new Error("Invalid date argument for getClosestWorkTime method");
4316
- }
4317
- const { dateBaseToCalculate, activityCalendar, direction, activity } = activityParams;
4318
- const newDate = activityCalendar.$gantt.getClosestWorkTime({
4319
- date: new Date(dateBaseToCalculate),
4320
- dir: direction,
4321
- unit: "hour",
4322
- task: activity
4323
- });
4324
- return newDate;
4325
- }
4326
-
4327
- // src/critical-path/legacy/helpers/filters.js
4328
- var byFirstLevel = (activity) => {
4329
- if (!activity) return false;
4330
- return activity["$level"] === 1;
4331
- };
4332
- var filterByParentType = (activity) => activity.type === "project";
4333
- var avoidSubproject = (activity) => activity["$level"] !== 0;
4334
- var byTaskType = (activity) => activity.type === "task";
4335
- var byTaskTypeAndMilestone = (activity) => activity.type === "task" || activity.type === "milestone";
4336
- var filters_default = {
4337
- byFirstLevel,
4338
- filterByParentType,
4339
- avoidSubproject,
4340
- byTaskType,
4341
- byTaskTypeAndMilestone
4342
- };
4343
-
4344
- // src/critical-path/legacy/constants/constraints.js
4345
- var CONSTRAINT_TYPES = Object.freeze({
4346
- MFO: "mfo",
4347
- MSO: "mso",
4348
- FNLT: "fnlt",
4349
- FNET: "fnet",
4350
- SNET: "snet",
4351
- SNLT: "snlt",
4352
- ALAP: "alap",
4353
- ASAP: "asap"
4354
- });
4355
-
4356
- // src/critical-path/legacy/constants/linkTypes.js
4357
- var LINK_TYPES = Object.freeze({
4358
- FS: "fs",
4359
- SS: "ss",
4360
- FF: "ff",
4361
- SF: "sf"
4362
- });
4363
-
4364
- // src/critical-path/legacy/constants/linkByCode.js
4365
- function getLinkNameByCode(code) {
4366
- const linkMap = {
4367
- 0: "fs",
4368
- 1: "ss",
4369
- 2: "ff",
4370
- 3: "sf"
4371
- };
4372
- return linkMap[Number(code)] || "sf";
4373
- }
4374
-
4375
- // src/critical-path/legacy/base/identifiers/secondLevelActivities.js
4376
- function identifySecondLevelActivities(params) {
4377
- const { gantt, pendingParentsWithNoLinks, calculations, linkProperty } = params;
4378
- try {
4379
- const subprojectsWithNoLinks = getSubprojectsWithNoLinks({
4380
- gantt,
4381
- pendingParentsWithNoLinks
4382
- });
4383
- if (subprojectsWithNoLinks.length === 0) {
4384
- return /* @__PURE__ */ new Set();
4385
- }
4386
- const allChildrenFromParents = getAllChildrenFromParents({
4387
- gantt,
4388
- subprojectsWithNoLinks
4389
- });
4390
- const childrenThatCanBeCalculated = filterChildrenThatCanBeCalculated({
4391
- gantt,
4392
- childrenIds: allChildrenFromParents,
4393
- linkProperty,
4394
- getArrayOfLinkedActivities,
4395
- calculations
4396
- });
4397
- return new Set(childrenThatCanBeCalculated.map((activity) => activity.id));
4398
- } catch (e) {
4399
- throw e;
4400
- }
4401
- }
4402
- function getSubprojectsWithNoLinks(params) {
4403
- const { gantt, pendingParentsWithNoLinks } = params;
4404
- return gantt.getTaskByTime().filter(filters_default.filterByParentType).filter((activity) => pendingParentsWithNoLinks.has(activity.id)).map((activity) => activity.id);
4405
- }
4406
- function getAllChildrenFromParents(params) {
4407
- const { gantt, subprojectsWithNoLinks } = params;
4408
- const allChildrenSet = /* @__PURE__ */ new Set();
4409
- for (const subprojectId of subprojectsWithNoLinks) {
4410
- const childrenIds = gantt.getChildren(subprojectId);
4411
- const validChildren = childrenIds.filter((childId) => {
4412
- const childActivity = gantt.getTask(childId);
4413
- const isTaskOrMilestone = filters_default.byTaskTypeAndMilestone(childActivity);
4414
- const hasProgressLessThan100 = Number(childActivity.progress) < 100;
4415
- return isTaskOrMilestone && hasProgressLessThan100;
4416
- });
4417
- validChildren.forEach((childId) => allChildrenSet.add(childId));
4418
- }
4419
- return [...allChildrenSet];
4420
- }
4421
- function filterChildrenThatCanBeCalculated(params) {
4422
- const {
4423
- gantt,
4424
- childrenIds,
4425
- linkProperty,
4426
- getArrayOfLinkedActivities: getArrayOfLinkedActivities3,
4427
- calculations
4428
- } = params;
4429
- const calculableChildren = [];
4430
- for (const childId of childrenIds) {
4431
- const activity = gantt.getTask(childId);
4432
- const links = activity[linkProperty] || [];
4433
- const linkedActivities = getArrayOfLinkedActivities3({
4434
- gantt,
4435
- links,
4436
- linkDirection: linkProperty === "$source" ? "target" : "source"
4437
- });
4438
- const isLinkedWithCalculatedOrSibling = linkedActivities.every(
4439
- (linkedActivityId) => calculations.has(linkedActivityId)
4440
- );
4441
- if (isLinkedWithCalculatedOrSibling) {
4442
- calculableChildren.push(activity);
4443
- }
4444
- }
4445
- return calculableChildren;
4446
- }
4447
- function getArrayOfLinkedActivities(params) {
4448
- const { gantt, links, linkDirection } = params;
4449
- const linkedActivityIds = [];
4450
- for (const linkId of links) {
4451
- const link = gantt.getLink(linkId);
4452
- if (!link) {
4453
- continue;
4454
- }
4455
- const linkedActivityId = Number(link[linkDirection]);
4456
- linkedActivityIds.push(linkedActivityId);
4457
- }
4458
- return linkedActivityIds;
4459
- }
4460
-
4461
- // src/critical-path/legacy/base/identifiers/initialActivities.js
4462
- function identifyInitialActivities({ gantt, linkProperty, direction }) {
4463
- try {
4464
- const chainStartActivities = getChainStartActivities(gantt, linkProperty);
4465
- if (direction === "forward") {
4466
- return {
4467
- chainStartActivities,
4468
- activitiesStartChainButChildrenOfLinkedParents: null
4469
- };
4470
- }
4471
- if (direction === "backward") {
4472
- return processBackwardActivities(
4473
- chainStartActivities,
4474
- gantt,
4475
- linkProperty
4476
- );
4477
- }
4478
- } catch (e) {
4479
- console.log("Critical Path", e);
4480
- throw e;
4481
- }
4482
- }
4483
- function getChainStartActivities(gantt, linkProperty) {
4484
- return gantt.getTaskByTime().filter(filters_default.avoidSubproject).filter(filters_default.byTaskTypeAndMilestone).filter((activity) => !activity[linkProperty].length);
4485
- }
4486
- function processBackwardActivities(chainStartActivities, gantt, linkProperty) {
4487
- const withoutLinkedParents = [];
4488
- const activitiesStartChainButChildrenOfLinkedParents = /* @__PURE__ */ new Set();
4489
- chainStartActivities.forEach((activity) => {
4490
- const parentId = Number(activity.parent);
4491
- const parentActivity = gantt.getTask(parentId);
4492
- if (!parentActivity) return;
4493
- if (parentActivity[linkProperty].length > 0) {
4494
- activitiesStartChainButChildrenOfLinkedParents.add(activity.id);
4495
- } else if (parentActivity.constraint_type !== "fnlt") {
4496
- withoutLinkedParents.push(activity);
4497
- }
4498
- });
4499
- return {
4500
- chainStartActivities: withoutLinkedParents,
4501
- activitiesStartChainButChildrenOfLinkedParents
4502
- };
4433
+ return {
4434
+ chainStartActivities: withoutLinkedParents,
4435
+ activitiesStartChainButChildrenOfLinkedParents
4436
+ };
4503
4437
  }
4504
4438
 
4505
4439
  // src/critical-path/legacy/base/identifiers/identifyParentsWithAndWithoutLinks.js
@@ -9491,13 +9425,18 @@ var NON_SCHEDULING_INLINE_COLUMNS = /* @__PURE__ */ new Set([
9491
9425
  COLUMN.SUBCONTRACT_ID,
9492
9426
  COLUMN.TAGS
9493
9427
  ]);
9494
- var PROP_ONLY_KINDS = /* @__PURE__ */ new Set([
9428
+ var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9495
9429
  "selection-toggle",
9496
9430
  "selection-replace",
9497
9431
  "visibility-set",
9498
9432
  "sir-sync",
9499
- "activity-lookahead-sync"
9433
+ "activity-lookahead-sync",
9434
+ "persistence-acknowledge",
9435
+ "baseline-apply",
9436
+ "ponderator-criterion-set",
9437
+ "status-criteria-set"
9500
9438
  ]);
9439
+ var NO_STRUCTURAL_EXEMPTIONS = /* @__PURE__ */ new Set();
9501
9440
  function editsOnlyNonSchedulingColumns(action) {
9502
9441
  if (action.kind === "inline-edit") {
9503
9442
  return NON_SCHEDULING_INLINE_COLUMNS.has(action.column);
@@ -9510,11 +9449,14 @@ function editsOnlyNonSchedulingColumns(action) {
9510
9449
  return false;
9511
9450
  }
9512
9451
  function isSchedulingDispatch(action, structuralStaleKinds) {
9513
- if (PROP_ONLY_KINDS.has(action.kind)) return false;
9452
+ if (NON_SCHEDULING_KINDS.has(action.kind)) return false;
9514
9453
  if (structuralStaleKinds.has(action.kind)) return false;
9515
9454
  if (editsOnlyNonSchedulingColumns(action)) return false;
9516
9455
  return true;
9517
9456
  }
9457
+ function dispatchChangesSchedulingState(action) {
9458
+ return isSchedulingDispatch(action, NO_STRUCTURAL_EXEMPTIONS);
9459
+ }
9518
9460
  function resolveRunCriticalPath(action) {
9519
9461
  return isSchedulingDispatch(
9520
9462
  action,
@@ -9590,6 +9532,9 @@ function resolveDerivedPasses(action, autoSchedule, options) {
9590
9532
  // src/dispatch/shared/post-mutation.ts
9591
9533
  async function runPostMutation(deps, args) {
9592
9534
  const passes = resolvePasses(args);
9535
+ if (passes.has(DERIVED_PASS.AUTOSCHEDULE)) {
9536
+ await refreshMutatedParentBounds(deps.adapter, args);
9537
+ }
9593
9538
  const schedule = passes.has(DERIVED_PASS.AUTOSCHEDULE) ? await runAutoschedule(deps, args.autoscheduleFrom) : emptySchedule();
9594
9539
  await rollUpParentBounds(deps.adapter, args, schedule.scheduledIds, passes);
9595
9540
  await runDerivedRecomputes(deps, passes, args.now);
@@ -9598,7 +9543,11 @@ async function runPostMutation(deps, args) {
9598
9543
  function resolvePasses(args) {
9599
9544
  const requested = args.autoscheduleFrom !== null;
9600
9545
  const ran = !args.options.skipAutoSchedule && requested;
9601
- return resolveDerivedPasses(args.action, { ran }, args.options);
9546
+ const passes = new Set(
9547
+ resolveDerivedPasses(args.action, { ran }, args.options)
9548
+ );
9549
+ if (args.recomputeParentProgress) passes.add(DERIVED_PASS.PARENT_PROGRESS);
9550
+ return passes;
9602
9551
  }
9603
9552
  async function runAutoschedule(deps, autoscheduleFrom) {
9604
9553
  const options = autoscheduleFrom === "roots" ? {} : { triggerId: autoscheduleFrom };
@@ -9609,20 +9558,23 @@ async function runAutoschedule(deps, autoscheduleFrom) {
9609
9558
  }
9610
9559
  function captureScheduledRows(adapter, updatedIds) {
9611
9560
  const scheduledIds = [];
9612
- const scheduledBeforeImages = /* @__PURE__ */ new Map();
9613
9561
  for (const id of updatedIds) {
9614
9562
  const activityId = String(id);
9615
9563
  scheduledIds.push(activityId);
9616
9564
  const snap = adapter.getActivity(activityId);
9617
9565
  if (snap) {
9618
- scheduledBeforeImages.set(activityId, structuredCloneActivity(snap));
9619
9566
  adapter.noteScheduledBefore(activityId, snap);
9620
9567
  }
9621
9568
  }
9622
- return { scheduledIds, scheduledBeforeImages };
9569
+ return { scheduledIds };
9623
9570
  }
9624
9571
  function emptySchedule() {
9625
- return { scheduledIds: [], scheduledBeforeImages: /* @__PURE__ */ new Map() };
9572
+ return { scheduledIds: [] };
9573
+ }
9574
+ async function refreshMutatedParentBounds(adapter, args) {
9575
+ const mutated = args.collectDirty ? args.collectDirty([]) : collectDirtyForStructural(args.recomputeParentsFrom, []);
9576
+ if (mutated.size === 0) return;
9577
+ await updateParentBoundsFromChildren(adapter, mutated, false);
9626
9578
  }
9627
9579
  async function rollUpParentBounds(adapter, args, scheduledIds, passes) {
9628
9580
  const dirty = args.collectDirty ? args.collectDirty(scheduledIds) : collectDirtyForStructural(args.recomputeParentsFrom, scheduledIds);
@@ -9631,17 +9583,20 @@ async function rollUpParentBounds(adapter, args, scheduledIds, passes) {
9631
9583
  dirty,
9632
9584
  passes.has(DERIVED_PASS.PARENT_PROGRESS),
9633
9585
  {
9634
- recomputeRealWork: passes.has(DERIVED_PASS.REAL_WORK)
9586
+ recomputeRealWork: passes.has(DERIVED_PASS.REAL_WORK),
9587
+ progressMode: args.recomputeParentProgress ? "zero-when-no-contribution" : "freeze-when-no-contribution"
9635
9588
  }
9636
9589
  );
9637
9590
  }
9638
9591
  async function runDerivedRecomputes(deps, passes, now) {
9639
9592
  if (passes.has(DERIVED_PASS.EXPECTED_PROGRESS) && now !== null) {
9640
- runExpectedProgressBase(
9641
- deps.adapter,
9642
- now,
9643
- deps.defaultBaseCalendarId ?? null
9644
- );
9593
+ if (hasActiveBaseline(deps.adapter)) {
9594
+ runExpectedProgressBase(
9595
+ deps.adapter,
9596
+ now,
9597
+ deps.defaultBaseCalendarId ?? null
9598
+ );
9599
+ }
9645
9600
  applyExpectedProgressLive(deps.adapter, now);
9646
9601
  applyStatusPass(deps.adapter, deps.sector.statusCriteria);
9647
9602
  }
@@ -9652,6 +9607,13 @@ async function runDerivedRecomputes(deps, passes, now) {
9652
9607
  await applyCriticalPath(deps.adapter, deps.sector.hoursPerDay);
9653
9608
  }
9654
9609
  }
9610
+ function hasActiveBaseline(adapter) {
9611
+ let active = false;
9612
+ adapter.forEachActivity((activity) => {
9613
+ if (getActiveBaseline(activity)) active = true;
9614
+ });
9615
+ return active;
9616
+ }
9655
9617
 
9656
9618
  // src/dispatch/handlers/revert-no-op-constraint-edit.ts
9657
9619
  var CONSTRAINT_EDIT_COLUMNS = /* @__PURE__ */ new Set([
@@ -9662,6 +9624,9 @@ var MUST_CONSTRAINTS = /* @__PURE__ */ new Set(["mso", "mfo"]);
9662
9624
  function isConstraintEditColumn(column) {
9663
9625
  return CONSTRAINT_EDIT_COLUMNS.has(column);
9664
9626
  }
9627
+ function isExplicitConstraintDateEdit(column) {
9628
+ return column === COLUMN.CONSTRAINT_DATE;
9629
+ }
9665
9630
  function revertNoOpConstraintEdit(activityId, priorConstraintDate, adapter, calendars) {
9666
9631
  const activity = adapter.getActivity(activityId);
9667
9632
  if (!activity) return false;
@@ -9705,7 +9670,7 @@ function sameDate(first, second) {
9705
9670
 
9706
9671
  // src/dispatch/shared/constraint-settle.ts
9707
9672
  async function settleConstraintEdits(deps, { postMutationArgs, revertTargets, revert }) {
9708
- let { scheduledIds, scheduledBeforeImages } = await runPostMutation(
9673
+ let { scheduledIds } = await runPostMutation(
9709
9674
  deps,
9710
9675
  withCriticalPathSuppressed(postMutationArgs)
9711
9676
  );
@@ -9720,15 +9685,11 @@ async function settleConstraintEdits(deps, { postMutationArgs, revertTargets, re
9720
9685
  withCriticalPathSuppressed(postMutationArgs)
9721
9686
  );
9722
9687
  scheduledIds = mergeScheduledIds(scheduledIds, resettled.scheduledIds);
9723
- scheduledBeforeImages = mergeScheduledBeforeImagesFirstWins(
9724
- scheduledBeforeImages,
9725
- resettled.scheduledBeforeImages
9726
- );
9727
9688
  }
9728
9689
  if (resolveRunCriticalPath(postMutationArgs.action)) {
9729
9690
  await runPostMutation(deps, criticalPathOnlyArgs(postMutationArgs));
9730
9691
  }
9731
- return { scheduledIds, scheduledBeforeImages };
9692
+ return { scheduledIds };
9732
9693
  }
9733
9694
  function withCriticalPathSuppressed(args) {
9734
9695
  return {
@@ -9750,13 +9711,6 @@ function mergeScheduledIds(firstSettleIds, secondSettleIds) {
9750
9711
  for (const scheduledId of secondSettleIds) merged.add(scheduledId);
9751
9712
  return [...merged];
9752
9713
  }
9753
- function mergeScheduledBeforeImagesFirstWins(firstSettleImages, secondSettleImages) {
9754
- const merged = new Map(firstSettleImages);
9755
- for (const [imageId, image] of secondSettleImages) {
9756
- if (!merged.has(imageId)) merged.set(imageId, image);
9757
- }
9758
- return merged;
9759
- }
9760
9714
 
9761
9715
  // src/dispatch/sir-auto-reject.ts
9762
9716
  function datesChanged(before, after) {
@@ -9791,33 +9745,12 @@ function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
9791
9745
 
9792
9746
  // src/dispatch/shared/change-set.ts
9793
9747
  function assembleChangeSet(adapter, args) {
9794
- const allTouched = /* @__PURE__ */ new Set();
9795
- for (const id of args.touchedIds) allTouched.add(id);
9796
- for (const id of args.scheduledIds) allTouched.add(id);
9797
- if (args.includeAllIds) {
9798
- for (const id of adapter.getAllIds()) allTouched.add(id);
9799
- }
9748
+ const allTouched = collectChangeSetIds(adapter, args);
9800
9749
  const before = new Map(args.beforeSnap);
9801
9750
  const captured = adapter.peekWriteCapture();
9802
9751
  if (captured) {
9803
- for (const id of captured.dirty) {
9804
- if (allTouched.has(id)) continue;
9805
- const beforeImg = captured.before.get(id);
9806
- const live = adapter.getActivity(id);
9807
- if (!beforeImg || !live) continue;
9808
- if (Object.keys(diffActivity(beforeImg, live)).length === 0) continue;
9809
- before.set(id, beforeImg);
9810
- allTouched.add(id);
9811
- }
9812
- }
9813
- if (args.scheduledBeforeImages) {
9814
- for (const [id, img] of args.scheduledBeforeImages) {
9815
- const sid = String(id);
9816
- if (!before.has(sid)) {
9817
- before.set(sid, img);
9818
- allTouched.add(sid);
9819
- }
9820
- }
9752
+ const insertedIds = collectInsertedIds(captured);
9753
+ mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
9821
9754
  }
9822
9755
  const beforeDiffEffects = args.sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
9823
9756
  const activityChanges = buildActivityChanges(adapter, before, allTouched);
@@ -9834,6 +9767,31 @@ function assembleChangeSet(adapter, args) {
9834
9767
  ...warnings.length > 0 ? { warnings } : {}
9835
9768
  };
9836
9769
  }
9770
+ function collectChangeSetIds(adapter, args) {
9771
+ const ids = /* @__PURE__ */ new Set();
9772
+ for (const id of args.touchedIds) ids.add(id);
9773
+ for (const id of args.scheduledIds) ids.add(id);
9774
+ if (args.includeAllIds) {
9775
+ for (const id of adapter.getAllIds()) ids.add(id);
9776
+ }
9777
+ return ids;
9778
+ }
9779
+ function mergeDirtyBeforeImages(adapter, captured, insertedIds, before, touched) {
9780
+ for (const id of captured.dirty) {
9781
+ if (insertedIds.has(id) || before.has(id)) continue;
9782
+ const beforeImage = captured.before.get(id);
9783
+ const live = adapter.getActivity(id);
9784
+ if (!beforeImage || !live) continue;
9785
+ if (Object.keys(diffActivity(beforeImage, live)).length === 0) continue;
9786
+ before.set(id, beforeImage);
9787
+ touched.add(id);
9788
+ }
9789
+ }
9790
+ function collectInsertedIds(captured) {
9791
+ return new Set(
9792
+ captured.ops.filter((operation) => operation.kind === "insert").map((operation) => operation.activityId)
9793
+ );
9794
+ }
9837
9795
 
9838
9796
  // src/dispatch/conversion.ts
9839
9797
  function snapshotToLink(l) {
@@ -10016,7 +9974,8 @@ async function dispatchInlineEdit(action, options, deps) {
10016
9974
  runPostProcessorsOnAdapter(
10017
9975
  action.activityId,
10018
9976
  changes.postProcessors ?? [],
10019
- adapter
9977
+ adapter,
9978
+ beforeSnap.get(String(action.activityId)) ?? null
10020
9979
  );
10021
9980
  const linkChanges = diffIncomingLinkLagChanges(
10022
9981
  beforeLinkLags,
@@ -10041,7 +10000,7 @@ async function dispatchInlineEdit(action, options, deps) {
10041
10000
  options
10042
10001
  });
10043
10002
  const isConstraintPath = isConstraintEditColumn(action.column);
10044
- const { scheduledIds, scheduledBeforeImages } = await (isConstraintPath ? settleConstraintEdits(
10003
+ const { scheduledIds } = await (isConstraintPath ? settleConstraintEdits(
10045
10004
  {
10046
10005
  adapter,
10047
10006
  scheduler,
@@ -10050,9 +10009,7 @@ async function dispatchInlineEdit(action, options, deps) {
10050
10009
  },
10051
10010
  {
10052
10011
  postMutationArgs: buildPostMutationArgs(),
10053
- revertTargets: [
10054
- { activityId: action.activityId, priorConstraintDate }
10055
- ],
10012
+ revertTargets: isExplicitConstraintDateEdit(action.column) ? [] : [{ activityId: action.activityId, priorConstraintDate }],
10056
10013
  revert: (activityId, capturedConstraintDate) => revertNoOpConstraintEdit(
10057
10014
  activityId,
10058
10015
  capturedConstraintDate,
@@ -10084,7 +10041,6 @@ async function dispatchInlineEdit(action, options, deps) {
10084
10041
  beforeSnap,
10085
10042
  touchedIds,
10086
10043
  scheduledIds,
10087
- scheduledBeforeImages,
10088
10044
  sirDetection: "before-diff",
10089
10045
  links: linkChanges,
10090
10046
  trackingEvents,
@@ -10098,6 +10054,40 @@ function columnInvalidatesExpandedLinks(column) {
10098
10054
  }
10099
10055
 
10100
10056
  // src/dispatch/handlers/link.ts
10057
+ function collectBatchContext(operations, adapter) {
10058
+ const activityIds = /* @__PURE__ */ new Set();
10059
+ const links = /* @__PURE__ */ new Map();
10060
+ for (const operation of operations) {
10061
+ if (operation.kind === "create") {
10062
+ activityIds.add(String(operation.source));
10063
+ activityIds.add(String(operation.target));
10064
+ continue;
10065
+ }
10066
+ const link = adapter.getLink(operation.linkId);
10067
+ if (!link) continue;
10068
+ activityIds.add(String(link.source));
10069
+ activityIds.add(String(link.target));
10070
+ links.set(operation.linkId, { ...link });
10071
+ }
10072
+ return { activityIds, links };
10073
+ }
10074
+ function applyBatchOperations(operations, adapter, linkIdGen, deterministicIds) {
10075
+ return operations.map((operation, index) => {
10076
+ const deterministicId = operation.kind === "create" ? deterministicIds?.get(index) : void 0;
10077
+ const result = applyLinkOperation(operation, {
10078
+ port: adapter,
10079
+ newLinkId: deterministicId ? () => deterministicId : linkIdGen.next
10080
+ });
10081
+ return {
10082
+ op: operation,
10083
+ finalLinkId: result.applied ? result.linkId : null,
10084
+ rejected: result.applied ? null : result.rejected ?? "link_op_rejected"
10085
+ };
10086
+ });
10087
+ }
10088
+ function stringKeyedLinkSnapshots(links) {
10089
+ return new Map([...links].map(([id, link]) => [String(id), link]));
10090
+ }
10101
10091
  async function dispatchLink(action, options, deps) {
10102
10092
  if (action.kind === "link-create" || action.kind === "link-update") {
10103
10093
  const type = action.type;
@@ -10155,40 +10145,14 @@ function batchOpToLinkOperation(op, hoursPerDay) {
10155
10145
  async function dispatchLinkBatch(operations, source, options, deps, deterministicIds) {
10156
10146
  const { adapter, scheduler, sector, linkIdGen } = deps;
10157
10147
  scheduler.invalidateAllCaches();
10158
- const affectedActivityIds = /* @__PURE__ */ new Set();
10159
- const beforeLinks = /* @__PURE__ */ new Map();
10160
- for (const op of operations) {
10161
- if (op.kind === "create") {
10162
- affectedActivityIds.add(String(op.source));
10163
- affectedActivityIds.add(String(op.target));
10164
- } else {
10165
- const link = adapter.getLink(op.linkId);
10166
- if (link) {
10167
- affectedActivityIds.add(String(link.source));
10168
- affectedActivityIds.add(String(link.target));
10169
- beforeLinks.set(op.linkId, { ...link });
10170
- }
10171
- }
10172
- }
10148
+ const { activityIds: affectedActivityIds, links: beforeLinks } = collectBatchContext(operations, adapter);
10173
10149
  const beforeActivities = snapshotActivities(adapter, affectedActivityIds);
10174
- const applied = [];
10175
- for (let i = 0; i < operations.length; i++) {
10176
- const op = operations[i];
10177
- const detId = op.kind === "create" && deterministicIds ? deterministicIds.get(i) : void 0;
10178
- const result = applyLinkOperation(op, {
10179
- port: adapter,
10180
- // Si el caller pasó un id deterministic (test override, o el bridge
10181
- // pasando el id que DHTMLX iba a usar), lo respetamos. Si no, el
10182
- // generador del core arranca desde Date.now() — mismo formato que
10183
- // DHTMLX uid(). Ver `generators/link-id-generator.ts`.
10184
- newLinkId: detId ? () => detId : linkIdGen.next
10185
- });
10186
- applied.push({
10187
- op,
10188
- finalLinkId: result.applied ? result.linkId : null,
10189
- rejected: result.applied ? null : result.rejected ?? "link_op_rejected"
10190
- });
10191
- }
10150
+ const applied = applyBatchOperations(
10151
+ operations,
10152
+ adapter,
10153
+ linkIdGen,
10154
+ deterministicIds
10155
+ );
10192
10156
  if (operations.length === 1 && applied[0] && applied[0].rejected !== null && source.kind !== "inline-edit") {
10193
10157
  return { ok: false, reason: applied[0].rejected };
10194
10158
  }
@@ -10208,21 +10172,24 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
10208
10172
  options
10209
10173
  }
10210
10174
  );
10211
- for (const id of scheduledIds) {
10212
- if (!beforeActivities.has(id)) {
10213
- const a = adapter.getActivity(id);
10214
- if (a) beforeActivities.set(id, structuredCloneActivity(a));
10215
- }
10216
- }
10217
10175
  const linkChanges = buildLinkChangesForBatch(applied, beforeLinks, adapter);
10218
- const beforeLinksByStringId = /* @__PURE__ */ new Map();
10219
- for (const [linkId, snap] of beforeLinks) {
10220
- beforeLinksByStringId.set(String(linkId), snap);
10221
- }
10222
10176
  return {
10223
10177
  ok: true,
10224
- __beforeLinks: beforeLinksByStringId,
10225
- changes: assembleChangeSet(adapter, {
10178
+ linkVerdicts: applied.map(
10179
+ (operation, operationIndex) => operation.rejected === null ? {
10180
+ operationIndex,
10181
+ kind: operation.op.kind,
10182
+ ok: true,
10183
+ linkId: String(operation.finalLinkId)
10184
+ } : {
10185
+ operationIndex,
10186
+ kind: operation.op.kind,
10187
+ ok: false,
10188
+ reason: operation.rejected
10189
+ }
10190
+ ),
10191
+ __beforeLinks: stringKeyedLinkSnapshots(beforeLinks),
10192
+ changes: assembleChangeSet(adapter, {
10226
10193
  source,
10227
10194
  beforeSnap: beforeActivities,
10228
10195
  touchedIds: affectedActivityIds,
@@ -10233,26 +10200,21 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
10233
10200
  };
10234
10201
  }
10235
10202
  function pickAutoscheduleTrigger(applied, beforeLinks, adapter) {
10236
- for (const a of applied) {
10237
- if (a.rejected !== null) continue;
10238
- if (a.op.kind === "create") {
10239
- return adapter.hasChildren(a.op.target) ? "roots" : String(a.op.target);
10240
- }
10241
- if (a.op.kind === "delete") {
10242
- const snap = beforeLinks.get(a.op.linkId);
10243
- if (snap) {
10244
- return adapter.hasChildren(snap.target) ? "roots" : String(snap.target);
10245
- }
10246
- }
10247
- if (a.op.kind === "update") {
10248
- const link = adapter.getLink(a.op.linkId);
10249
- if (link) {
10250
- return adapter.hasChildren(link.target) ? "roots" : String(link.target);
10251
- }
10252
- }
10203
+ for (const operation of applied) {
10204
+ if (operation.rejected !== null) continue;
10205
+ const target = autoscheduleTarget(operation.op, beforeLinks, adapter);
10206
+ if (target !== null)
10207
+ return adapter.hasChildren(target) ? "roots" : String(target);
10253
10208
  }
10254
10209
  return null;
10255
10210
  }
10211
+ function autoscheduleTarget(operation, beforeLinks, adapter) {
10212
+ if (operation.kind === "create") return operation.target;
10213
+ if (operation.kind === "delete") {
10214
+ return beforeLinks.get(operation.linkId)?.target ?? null;
10215
+ }
10216
+ return adapter.getLink(operation.linkId)?.target ?? null;
10217
+ }
10256
10218
  function toLinkOperation(action, hoursPerDay) {
10257
10219
  if (action.kind === "link-create") {
10258
10220
  return {
@@ -10323,6 +10285,128 @@ var DISPATCH_TRACK_EVENT = {
10323
10285
  ACTIVITY_OUTDENT: "schedule_activity_outdent"
10324
10286
  };
10325
10287
 
10288
+ // src/internal/hierarchy/visual-order.ts
10289
+ var NOT_SET_SORT_VALUE = Number.MAX_SAFE_INTEGER;
10290
+ function getChildrenInVisualOrder(parentId, adapter) {
10291
+ const children = collectChildrenSnapshots(parentId, adapter);
10292
+ children.sort(compareActivitiesInVisualOrder);
10293
+ return children.map((a) => String(a.id));
10294
+ }
10295
+ function iterateInVisualOrder(adapter) {
10296
+ const result = [];
10297
+ const visit = (activity) => {
10298
+ result.push(activity);
10299
+ const childIds = getChildrenInVisualOrder(String(activity.id), adapter);
10300
+ for (const childId of childIds) {
10301
+ const child = adapter.getActivity(childId);
10302
+ if (child) visit(child);
10303
+ }
10304
+ };
10305
+ const rootIds = getChildrenInVisualOrder(ROOT_PARENT_ID, adapter);
10306
+ for (const rootId of rootIds) {
10307
+ const root = adapter.getActivity(rootId);
10308
+ if (root) visit(root);
10309
+ }
10310
+ return result;
10311
+ }
10312
+ function findPreviousNonSelectedSibling(taskId, selected, adapter) {
10313
+ const activity = adapter.getActivity(taskId);
10314
+ if (!activity) return null;
10315
+ const parentKey = parentKeyOf(activity);
10316
+ const siblings = getChildrenInVisualOrder(parentKey, adapter);
10317
+ const idx = siblings.findIndex((id) => String(id) === String(taskId));
10318
+ if (idx <= 0) return null;
10319
+ for (let i = idx - 1; i >= 0; i--) {
10320
+ const candidateId = siblings[i];
10321
+ if (candidateId === void 0) continue;
10322
+ if (!setHas(selected, candidateId)) return candidateId;
10323
+ }
10324
+ return null;
10325
+ }
10326
+ function visualIndexInParent(taskId, adapter) {
10327
+ const activity = adapter.getActivity(taskId);
10328
+ if (!activity) return -1;
10329
+ const parentKey = parentKeyOf(activity);
10330
+ const siblings = getChildrenInVisualOrder(parentKey, adapter);
10331
+ return siblings.findIndex((id) => String(id) === String(taskId));
10332
+ }
10333
+ function collectChildrenSnapshots(parentId, adapter) {
10334
+ const key = String(parentId);
10335
+ if (key === "0") {
10336
+ return adapter.getAllActivities().filter((a) => a.parentId === null);
10337
+ }
10338
+ const childIds = adapter.getChildren(parentId);
10339
+ const out = [];
10340
+ for (const id of childIds) {
10341
+ const snap = adapter.getActivity(id);
10342
+ if (snap) out.push(snap);
10343
+ }
10344
+ return out;
10345
+ }
10346
+ function compareActivitiesInVisualOrder(a, b) {
10347
+ const ac = correlativeIdOf(a);
10348
+ const bc = correlativeIdOf(b);
10349
+ if (ac !== bc) return ac - bc;
10350
+ return String(a.id).localeCompare(String(b.id));
10351
+ }
10352
+ function correlativeIdOf(activity) {
10353
+ const raw = activity.correlativeId;
10354
+ const n = typeof raw === "number" ? raw : Number(raw);
10355
+ return Number.isFinite(n) ? n : NOT_SET_SORT_VALUE;
10356
+ }
10357
+ function parentKeyOf(activity) {
10358
+ const parentId = activity.parentId;
10359
+ if (parentId === null) return "0";
10360
+ return parentId;
10361
+ }
10362
+ function setHas(set, id) {
10363
+ if (set.has(id)) return true;
10364
+ return set.has(String(id));
10365
+ }
10366
+
10367
+ // src/internal/hierarchy/recompute-correlative-ids.ts
10368
+ function recomputeCorrelativeIds(adapter) {
10369
+ adapter.invalidateVisualOrderIds?.();
10370
+ const roots = [];
10371
+ adapter.forEachActivity((activity) => {
10372
+ if (isRootParent(activity.parentId)) roots.push(activity);
10373
+ });
10374
+ roots.sort(byCorrelativeId);
10375
+ const stack = [];
10376
+ pushInReverseOrder(stack, roots);
10377
+ const shifts = [];
10378
+ let counter = 0;
10379
+ while (stack.length > 0) {
10380
+ const activity = stack.pop();
10381
+ if (activity.correlativeId !== counter) {
10382
+ const before = typeof activity.correlativeId === "number" ? activity.correlativeId : void 0;
10383
+ shifts.push({ activityId: String(activity.id), before });
10384
+ if (before !== void 0)
10385
+ adapter.noteCorrelativeBefore?.(String(activity.id), before);
10386
+ setCorrelativeId(activity, counter);
10387
+ }
10388
+ counter += 1;
10389
+ const children = collectChildrenSnapshots(String(activity.id), adapter);
10390
+ children.sort(byCorrelativeId);
10391
+ pushInReverseOrder(stack, children);
10392
+ }
10393
+ return shifts;
10394
+ }
10395
+ function pushInReverseOrder(stack, activities) {
10396
+ for (let i = activities.length - 1; i >= 0; i -= 1) {
10397
+ stack.push(activities[i]);
10398
+ }
10399
+ }
10400
+ function getCorrelativeId(activity) {
10401
+ const raw = activity.correlativeId;
10402
+ const n = typeof raw === "number" ? raw : Number(raw);
10403
+ return Number.isFinite(n) ? n : NOT_SET_SORT_VALUE;
10404
+ }
10405
+ var byCorrelativeId = (a, b) => getCorrelativeId(a) - getCorrelativeId(b);
10406
+ function setCorrelativeId(activity, value) {
10407
+ activity.correlativeId = value;
10408
+ }
10409
+
10326
10410
  // src/propagations/upward/recompute-hh-cascade.ts
10327
10411
  function recomputeHhCascadeForParent(startParentId, adapter) {
10328
10412
  if (!startParentId || startParentId === "0") return;
@@ -10477,8 +10561,7 @@ var NEW_ACTIVITY_DEFAULTS = Object.freeze({
10477
10561
  expectedProgress: null,
10478
10562
  expectedProgressBaseline: null,
10479
10563
  status: null,
10480
- criticalPath: null,
10481
- promotionRestore: null
10564
+ criticalPath: null
10482
10565
  });
10483
10566
  var FIRST_ACTIVITY_TEXT = "New Master Plan";
10484
10567
  var NEW_ACTIVITY_TEXT = "New Activity";
@@ -10583,6 +10666,23 @@ function resolveStartDate(parent, fallback, override) {
10583
10666
  }
10584
10667
 
10585
10668
  // src/creation/parent-mutation-pipeline.ts
10669
+ function snapshotOf(parent) {
10670
+ const type = parent.type;
10671
+ if (type !== "task" && type !== "milestone") return null;
10672
+ const startDate = parent.startDate;
10673
+ const endDate = parent.endDate;
10674
+ if (!(startDate instanceof Date) || !(endDate instanceof Date)) return null;
10675
+ return {
10676
+ type,
10677
+ startDate: new Date(startDate),
10678
+ endDate: new Date(endDate),
10679
+ durationHours: Number(parent.durationHours ?? 0),
10680
+ expectedProgressBaseline: typeof parent.expectedProgressBaseline === "number" ? parent.expectedProgressBaseline : null,
10681
+ constraintType: parent.constraintType ?? null,
10682
+ constraintDate: parent.constraintDate instanceof Date ? new Date(parent.constraintDate) : null,
10683
+ progress: Number(parent.progress ?? 0)
10684
+ };
10685
+ }
10586
10686
  function buildParentMutations(input) {
10587
10687
  const parent = input.getParent(input.parentId);
10588
10688
  if (!parent) return null;
@@ -10601,14 +10701,18 @@ function buildParentMutations(input) {
10601
10701
  }
10602
10702
  if (wasPromotableLeaf) {
10603
10703
  fields.type = PROMOTION_TARGET_TYPE;
10604
- fields.constraintType = CONSTRAINT_TYPE2.ASAP;
10605
- fields.constraintDate = null;
10606
10704
  if ((input.promotionSource ?? "create") === "create") {
10705
+ fields.constraintType = CONSTRAINT_TYPE2.ASAP;
10706
+ fields.constraintDate = null;
10607
10707
  fields.progress = 0;
10608
10708
  fields.expectedProgressBaseline = 0;
10609
10709
  }
10610
10710
  }
10611
- return { parentId: input.parentId, fields };
10711
+ return {
10712
+ parentId: input.parentId,
10713
+ fields,
10714
+ promotionSnapshot: snapshotOf(parent)
10715
+ };
10612
10716
  }
10613
10717
  function isPromotableLeaf(parent) {
10614
10718
  return isPromotableLeafType(parent.type) && (!Array.isArray(parent.newActivityIds) || parent.newActivityIds.length === 0);
@@ -10834,6 +10938,12 @@ function applyParentMutations(action, newId, parent, adapter, preserveParentCust
10834
10938
  if (skipCustomIdMutation) continue;
10835
10939
  setActivityFieldDynamic(adapter, parentMutations.parentId, key, value);
10836
10940
  }
10941
+ if (parentMutations.promotionSnapshot) {
10942
+ adapter.setPromotionSnapshot(
10943
+ parentMutations.parentId,
10944
+ parentMutations.promotionSnapshot
10945
+ );
10946
+ }
10837
10947
  }
10838
10948
  function recomputeAndFoldCorrelatives(adapter, newId, beforeSnap, initialTouched, opts) {
10839
10949
  if (opts.skipCorrelativeRecompute) return;
@@ -10847,7 +10957,7 @@ function recomputeAndFoldCorrelatives(adapter, newId, beforeSnap, initialTouched
10847
10957
  initialTouched
10848
10958
  );
10849
10959
  }
10850
- function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPerDay, scheduledBeforeImages) {
10960
+ function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPerDay) {
10851
10961
  const { newId, newActivity, beforeSnap, initialTouched } = coreResult;
10852
10962
  const trackingEvent = {
10853
10963
  name: DISPATCH_TRACK_EVENT.ACTIVITY_CREATION,
@@ -10860,40 +10970,13 @@ function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPe
10860
10970
  if (action.eventSource !== void 0) {
10861
10971
  trackingEvent.properties.event_source = action.eventSource;
10862
10972
  }
10863
- const existingScheduledBeforeImages = scheduledBeforeImages ? new Map(
10864
- [...scheduledBeforeImages].filter(
10865
- ([scheduledId]) => String(scheduledId) !== String(newId)
10866
- )
10867
- ) : void 0;
10868
10973
  return assembleChangeSet(adapter, {
10869
10974
  source: action,
10870
10975
  beforeSnap,
10871
10976
  touchedIds: initialTouched,
10872
10977
  scheduledIds,
10873
- scheduledBeforeImages: existingScheduledBeforeImages,
10874
10978
  trackingEvents: [trackingEvent]});
10875
10979
  }
10876
- function createActivitySync(action, deps) {
10877
- const result = createActivityCore(action, deps);
10878
- if (!result.ok) {
10879
- return { ok: false, reason: result.reason };
10880
- }
10881
- recomputeParentBoundsSync(deps.adapter, /* @__PURE__ */ new Set([result.newId]));
10882
- const snapshot = deps.adapter.getActivity(result.newId);
10883
- if (!snapshot) {
10884
- throw new Error(
10885
- `[ScheduleCore] createActivity: new activity ${result.newId} missing after commit`
10886
- );
10887
- }
10888
- const changes = buildCreateChangeSet(
10889
- deps.adapter,
10890
- action,
10891
- result,
10892
- [],
10893
- deps.sector.hoursPerDay
10894
- );
10895
- return { ok: true, activity: snapshot, changes };
10896
- }
10897
10980
  async function dispatchActivityCreate(action, options, deps) {
10898
10981
  const { adapter, scheduler, sector } = deps;
10899
10982
  const core = createActivityCore(action, deps);
@@ -10921,14 +11004,13 @@ async function dispatchActivityCreate(action, options, deps) {
10921
11004
  } finally {
10922
11005
  adapter.setActivityField(newId, "autoScheduling", createdAutoScheduling);
10923
11006
  }
10924
- const { scheduledIds, scheduledBeforeImages } = scheduleOutcome;
11007
+ const { scheduledIds } = scheduleOutcome;
10925
11008
  const changeset = buildCreateChangeSet(
10926
11009
  adapter,
10927
11010
  action,
10928
11011
  core,
10929
11012
  scheduledIds,
10930
- sector.hoursPerDay,
10931
- scheduledBeforeImages
11013
+ sector.hoursPerDay
10932
11014
  );
10933
11015
  return { ok: true, changes: changeset };
10934
11016
  }
@@ -11073,7 +11155,7 @@ async function dispatchActivityPaste(action, options, deps) {
11073
11155
  rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
11074
11156
  });
11075
11157
  }
11076
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11158
+ const { scheduledIds } = await runPostMutation(
11077
11159
  {
11078
11160
  adapter,
11079
11161
  scheduler,
@@ -11100,14 +11182,6 @@ async function dispatchActivityPaste(action, options, deps) {
11100
11182
  beforeSnap,
11101
11183
  touchedIds: touched,
11102
11184
  scheduledIds,
11103
- // Existing rows the autoscheduler moved need a before-image (the global
11104
- // beforeSnap used to supply it). Pass scheduledBeforeImages but filter out
11105
- // the pasted rows — their pre-schedule image would flip them to `updated`.
11106
- scheduledBeforeImages: new Map(
11107
- [...scheduledBeforeImages].filter(
11108
- ([schedId]) => !createdIdSet.has(String(schedId))
11109
- )
11110
- ),
11111
11185
  links: linkChanges,
11112
11186
  hoursPerDay: sector.hoursPerDay
11113
11187
  })
@@ -11242,10 +11316,8 @@ function collectIncidentLinkIds(activityIds, adapter) {
11242
11316
  }
11243
11317
 
11244
11318
  // src/internal/hierarchy/parent-demotion.ts
11245
- function restoredPromotionFields(parent) {
11246
- const restore = parent.promotionRestore;
11247
- if (typeof restore !== "object" || restore === null) return null;
11248
- if (restore.type !== "task" && restore.type !== "milestone") return null;
11319
+ function restoredPromotionFields(restore) {
11320
+ if (!restore) return null;
11249
11321
  return {
11250
11322
  type: restore.type,
11251
11323
  startDate: new Date(restore.startDate),
@@ -11254,16 +11326,20 @@ function restoredPromotionFields(parent) {
11254
11326
  expectedProgressBaseline: restore.expectedProgressBaseline,
11255
11327
  constraintType: restore.constraintType,
11256
11328
  constraintDate: restore.constraintDate ? new Date(restore.constraintDate) : null,
11257
- progress: restore.progress,
11258
- promotionRestore: null
11329
+ progress: restore.progress
11259
11330
  };
11260
11331
  }
11332
+ function canonicalEndDateField(parent, durationHours, computeEndDate) {
11333
+ const startDate = parent.startDate;
11334
+ if (!computeEndDate || !(startDate instanceof Date)) return {};
11335
+ return { endDate: computeEndDate(startDate, durationHours) };
11336
+ }
11261
11337
  function buildParentDemotionMutations(input) {
11262
11338
  if (input.remainingChildIds.length > 0) return null;
11263
11339
  const parent = input.parent;
11264
11340
  const canonicalDuration = input.defaultDurationHours;
11265
11341
  const hasCanonicalDefaults = Number.isFinite(canonicalDuration);
11266
- const fields = restoredPromotionFields(parent) ?? (hasCanonicalDefaults ? {
11342
+ const fields = restoredPromotionFields(input.promotionSnapshot) ?? (hasCanonicalDefaults ? {
11267
11343
  type: DEMOTION_TARGET_TYPE,
11268
11344
  durationHours: canonicalDuration,
11269
11345
  progress: 0,
@@ -11271,7 +11347,11 @@ function buildParentDemotionMutations(input) {
11271
11347
  constraintType: "asap",
11272
11348
  constraintDate: null,
11273
11349
  hasNewActivities: false,
11274
- promotionRestore: null
11350
+ ...canonicalEndDateField(
11351
+ parent,
11352
+ canonicalDuration,
11353
+ input.computeEndDate
11354
+ )
11275
11355
  } : { type: DEMOTION_TARGET_TYPE });
11276
11356
  if (input.idsRemoved && input.idsRemoved.size > 0) {
11277
11357
  const existing = Array.isArray(parent.newActivityIds) ? parent.newActivityIds : [];
@@ -11368,6 +11448,8 @@ async function dispatchActivityDelete(action, options, deps) {
11368
11448
  parent,
11369
11449
  remainingChildIds: remainingChildren.map(String),
11370
11450
  defaultDurationHours: sector.hoursPerDay,
11451
+ promotionSnapshot: adapter.getPromotionSnapshot(parentId),
11452
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({ startDate, durationHours, task: parent }),
11371
11453
  idsRemoved: toDelete
11372
11454
  });
11373
11455
  if (!demotion) {
@@ -11403,7 +11485,7 @@ async function dispatchActivityDelete(action, options, deps) {
11403
11485
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11404
11486
  beforeSnap.set(shift.activityId, beforeImage);
11405
11487
  }
11406
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11488
+ const { scheduledIds } = await runPostMutation(
11407
11489
  {
11408
11490
  adapter,
11409
11491
  scheduler,
@@ -11440,7 +11522,6 @@ async function dispatchActivityDelete(action, options, deps) {
11440
11522
  beforeSnap,
11441
11523
  touchedIds,
11442
11524
  scheduledIds,
11443
- scheduledBeforeImages,
11444
11525
  sirDetection: "after-diff",
11445
11526
  links: buildLinkDeletions(beforeLinks),
11446
11527
  trackingEvents: [trackingEvent],
@@ -11612,6 +11693,12 @@ async function dispatchActivityMove(action, options, deps) {
11612
11693
  for (const [key, value] of Object.entries(promotion.fields)) {
11613
11694
  setActivityFieldDynamic(adapter, promotion.parentId, key, value);
11614
11695
  }
11696
+ if (promotion.promotionSnapshot) {
11697
+ adapter.setPromotionSnapshot(
11698
+ promotion.parentId,
11699
+ promotion.promotionSnapshot
11700
+ );
11701
+ }
11615
11702
  if (promotion.fields.customId === null) {
11616
11703
  releaseClearedCustomId(oldNewParentCustomId, deps.customIdTracker);
11617
11704
  }
@@ -11626,6 +11713,12 @@ async function dispatchActivityMove(action, options, deps) {
11626
11713
  parent: oldParentActivity,
11627
11714
  remainingChildIds: remaining.map(String),
11628
11715
  defaultDurationHours: sector.hoursPerDay,
11716
+ promotionSnapshot: adapter.getPromotionSnapshot(oldParentKey),
11717
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({
11718
+ startDate,
11719
+ durationHours,
11720
+ task: oldParentActivity
11721
+ }),
11629
11722
  idsRemoved: /* @__PURE__ */ new Set([action.activityId])
11630
11723
  });
11631
11724
  if (demotion) {
@@ -11665,7 +11758,8 @@ async function dispatchActivityMove(action, options, deps) {
11665
11758
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11666
11759
  beforeSnap.set(shift.activityId, beforeImage);
11667
11760
  }
11668
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11761
+ const sourceParentDirtyIds = parentChanged && oldParentKey !== ROOT_PARENT_ID ? adapter.getChildren(oldParentKey).map(String) : [];
11762
+ const { scheduledIds } = await runPostMutation(
11669
11763
  {
11670
11764
  adapter,
11671
11765
  scheduler,
@@ -11675,9 +11769,14 @@ async function dispatchActivityMove(action, options, deps) {
11675
11769
  {
11676
11770
  action,
11677
11771
  autoscheduleFrom: action.activityId,
11678
- recomputeParentsFrom: [action.activityId, action.parentId],
11772
+ recomputeParentsFrom: [
11773
+ action.activityId,
11774
+ action.parentId,
11775
+ ...sourceParentDirtyIds
11776
+ ],
11679
11777
  now: deps.now,
11680
- options
11778
+ options,
11779
+ recomputeParentProgress: parentChanged
11681
11780
  }
11682
11781
  );
11683
11782
  const invalidLinkIds = parentChanged ? collectInvalidLinkIdsForCycle([action.activityId], adapter) : [];
@@ -11701,7 +11800,6 @@ async function dispatchActivityMove(action, options, deps) {
11701
11800
  beforeSnap,
11702
11801
  touchedIds: touched,
11703
11802
  scheduledIds,
11704
- scheduledBeforeImages,
11705
11803
  sirDetection: "before-diff",
11706
11804
  trackingEvents: [trackingEvent],
11707
11805
  hoursPerDay: sector.hoursPerDay
@@ -11791,6 +11889,12 @@ async function dispatchActivityIndent(action, options, deps) {
11791
11889
  for (const [key, value] of Object.entries(promotion.fields)) {
11792
11890
  setActivityFieldDynamic(adapter, promotion.parentId, key, value);
11793
11891
  }
11892
+ if (promotion.promotionSnapshot) {
11893
+ adapter.setPromotionSnapshot(
11894
+ promotion.parentId,
11895
+ promotion.promotionSnapshot
11896
+ );
11897
+ }
11794
11898
  if (!wasTrackedAsNewChild) {
11795
11899
  adapter.setActivityField(
11796
11900
  newParentId,
@@ -11836,7 +11940,7 @@ async function dispatchActivityIndent(action, options, deps) {
11836
11940
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11837
11941
  beforeSnap.set(shift.activityId, beforeImage);
11838
11942
  }
11839
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11943
+ const { scheduledIds } = await runPostMutation(
11840
11944
  {
11841
11945
  adapter,
11842
11946
  scheduler,
@@ -11893,12 +11997,21 @@ async function dispatchActivityIndent(action, options, deps) {
11893
11997
  }
11894
11998
  return {
11895
11999
  ok: true,
12000
+ activityVerdicts: action.activityIds.map((activityId) => {
12001
+ const failure = failed.find(
12002
+ (entry) => String(entry.activityId) === String(activityId)
12003
+ );
12004
+ return failure ? {
12005
+ activityId: String(activityId),
12006
+ ok: false,
12007
+ reason: failure.reason
12008
+ } : { activityId: String(activityId), ok: true };
12009
+ }),
11896
12010
  changes: assembleChangeSet(adapter, {
11897
12011
  source: action,
11898
12012
  beforeSnap,
11899
12013
  touchedIds: touched,
11900
12014
  scheduledIds,
11901
- scheduledBeforeImages,
11902
12015
  trackingEvents: [trackingEvent],
11903
12016
  hoursPerDay: sector.hoursPerDay
11904
12017
  })
@@ -12020,6 +12133,12 @@ async function dispatchActivityOutdent(action, options, deps) {
12020
12133
  parent: parentActivity,
12021
12134
  remainingChildIds: remaining.map(String),
12022
12135
  defaultDurationHours: sector.hoursPerDay,
12136
+ promotionSnapshot: adapter.getPromotionSnapshot(oldParentKey),
12137
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({
12138
+ startDate,
12139
+ durationHours,
12140
+ task: parentActivity
12141
+ }),
12023
12142
  idsRemoved: new Set(
12024
12143
  planned.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
12025
12144
  )
@@ -12065,7 +12184,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12065
12184
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
12066
12185
  beforeSnap.set(shift.activityId, beforeImage);
12067
12186
  }
12068
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
12187
+ const { scheduledIds } = await runPostMutation(
12069
12188
  {
12070
12189
  adapter,
12071
12190
  scheduler,
@@ -12098,12 +12217,21 @@ async function dispatchActivityOutdent(action, options, deps) {
12098
12217
  }
12099
12218
  return {
12100
12219
  ok: true,
12220
+ activityVerdicts: action.activityIds.map((activityId) => {
12221
+ const failure = failed.find(
12222
+ (entry) => String(entry.activityId) === String(activityId)
12223
+ );
12224
+ return failure ? {
12225
+ activityId: String(activityId),
12226
+ ok: false,
12227
+ reason: failure.reason
12228
+ } : { activityId: String(activityId), ok: true };
12229
+ }),
12101
12230
  changes: assembleChangeSet(adapter, {
12102
12231
  source: action,
12103
12232
  beforeSnap,
12104
12233
  touchedIds: touched,
12105
12234
  scheduledIds,
12106
- scheduledBeforeImages,
12107
12235
  trackingEvents: [trackingEvent],
12108
12236
  hoursPerDay: sector.hoursPerDay
12109
12237
  })
@@ -12195,11 +12323,15 @@ async function dispatchDatesBatch(action, options, deps) {
12195
12323
  mergeBeforeSnapshots(beforeSnap, editTouched, deps);
12196
12324
  for (const id of editTouched) touchedIds.add(id);
12197
12325
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12326
+ const preEditSnapshot = snapshotActivities(adapter, /* @__PURE__ */ new Set([String(edit.activityId)])).get(
12327
+ String(edit.activityId)
12328
+ ) ?? null;
12198
12329
  applyFieldChanges(adapter, edit.activityId, outcome.changes);
12199
12330
  runPostProcessorsOnAdapter(
12200
12331
  edit.activityId,
12201
12332
  outcome.changes.postProcessors ?? [],
12202
- adapter
12333
+ adapter,
12334
+ preEditSnapshot
12203
12335
  );
12204
12336
  linkChanges.push(
12205
12337
  ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
@@ -12334,6 +12466,7 @@ async function dispatchBulkEdit(action, options, deps) {
12334
12466
  const verdicts = [];
12335
12467
  const beforeSnap = /* @__PURE__ */ new Map();
12336
12468
  const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12469
+ const explicitDateEditActivities = /* @__PURE__ */ new Set();
12337
12470
  const touchedIds = /* @__PURE__ */ new Set();
12338
12471
  const trackingEvents = [];
12339
12472
  const linkChanges = [];
@@ -12352,14 +12485,21 @@ async function dispatchBulkEdit(action, options, deps) {
12352
12485
  mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12353
12486
  for (const touchedId of editTouched) touchedIds.add(touchedId);
12354
12487
  recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12488
+ if (isExplicitConstraintDateEdit(edit.column)) {
12489
+ explicitDateEditActivities.add(String(edit.activityId));
12490
+ }
12355
12491
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12492
+ const preEditSnapshot = snapshotActivities(adapter, /* @__PURE__ */ new Set([String(edit.activityId)])).get(
12493
+ String(edit.activityId)
12494
+ ) ?? null;
12356
12495
  const customIdBeforeApply = readLiveCustomId(edit, deps);
12357
12496
  const changes = outcome.changes;
12358
12497
  applyFieldChanges(adapter, edit.activityId, changes);
12359
12498
  runPostProcessorsOnAdapter(
12360
12499
  edit.activityId,
12361
12500
  outcome.changes.postProcessors ?? [],
12362
- adapter
12501
+ adapter,
12502
+ preEditSnapshot
12363
12503
  );
12364
12504
  syncCustomIdTracker(edit, customIdBeforeApply, deps);
12365
12505
  linkChanges.push(
@@ -12381,7 +12521,7 @@ async function dispatchBulkEdit(action, options, deps) {
12381
12521
  options
12382
12522
  };
12383
12523
  const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12384
- const { scheduledIds, scheduledBeforeImages } = await (batchHasConstraintEdits ? settleConstraintEdits(
12524
+ const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
12385
12525
  {
12386
12526
  adapter,
12387
12527
  scheduler,
@@ -12390,12 +12530,12 @@ async function dispatchBulkEdit(action, options, deps) {
12390
12530
  },
12391
12531
  {
12392
12532
  postMutationArgs,
12393
- revertTargets: [...constraintPriorsByActivity].map(
12394
- ([activityId, priorConstraintDate]) => ({
12395
- activityId,
12396
- priorConstraintDate
12397
- })
12398
- ),
12533
+ revertTargets: [...constraintPriorsByActivity].filter(
12534
+ ([activityId]) => !explicitDateEditActivities.has(activityId)
12535
+ ).map(([activityId, priorConstraintDate]) => ({
12536
+ activityId,
12537
+ priorConstraintDate
12538
+ })),
12399
12539
  revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12400
12540
  activityId,
12401
12541
  priorConstraintDate,
@@ -12423,7 +12563,6 @@ async function dispatchBulkEdit(action, options, deps) {
12423
12563
  // Los dependientes que mueve el autoscheduler no están en beforeSnap;
12424
12564
  // sin su before-image el diff los marca FALSE-CREATED y el undo los
12425
12565
  // borra (fix 4bbccbc de inline-edit).
12426
- scheduledBeforeImages,
12427
12566
  sirDetection: "before-diff",
12428
12567
  links: linkChanges,
12429
12568
  trackingEvents,
@@ -12558,8 +12697,515 @@ function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
12558
12697
  }
12559
12698
  }
12560
12699
 
12700
+ // src/dispatch/handlers/persistence-acknowledge.ts
12701
+ function assertValidAssignment(assignment, entityName, readCurrent, ownerByBackendId, seenIds, seenBackendIds) {
12702
+ const id = String(assignment.id);
12703
+ if (id.length === 0 || seenIds.has(id)) {
12704
+ throw new Error(
12705
+ `persistence-acknowledge: duplicate or empty ${entityName} id "${id}"`
12706
+ );
12707
+ }
12708
+ if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0) {
12709
+ throw new Error(
12710
+ `persistence-acknowledge: invalid ${entityName} proplannerId ${String(assignment.proplannerId)}`
12711
+ );
12712
+ }
12713
+ if (seenBackendIds.has(assignment.proplannerId)) {
12714
+ throw new Error(
12715
+ `persistence-acknowledge: duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
12716
+ );
12717
+ }
12718
+ const state = readCurrent(id);
12719
+ if (!state.exists) {
12720
+ throw new Error(
12721
+ `persistence-acknowledge: unknown ${entityName} id "${id}"`
12722
+ );
12723
+ }
12724
+ if (state.current !== null && state.current !== assignment.proplannerId) {
12725
+ throw new Error(
12726
+ `persistence-acknowledge: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
12727
+ );
12728
+ }
12729
+ const existingOwner = ownerByBackendId.get(assignment.proplannerId);
12730
+ if (existingOwner !== void 0 && existingOwner !== id) {
12731
+ throw new Error(
12732
+ `persistence-acknowledge: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
12733
+ );
12734
+ }
12735
+ seenIds.add(id);
12736
+ seenBackendIds.add(assignment.proplannerId);
12737
+ }
12738
+ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
12739
+ const seenIds = /* @__PURE__ */ new Set();
12740
+ const seenBackendIds = /* @__PURE__ */ new Set();
12741
+ for (const assignment of assignments) {
12742
+ assertValidAssignment(
12743
+ assignment,
12744
+ entityName,
12745
+ readCurrent,
12746
+ ownerByBackendId,
12747
+ seenIds,
12748
+ seenBackendIds
12749
+ );
12750
+ }
12751
+ }
12752
+ function dispatchPersistenceAcknowledge(action, deps) {
12753
+ const activities = action.activities ?? [];
12754
+ const links = action.links ?? [];
12755
+ const activityOwnerByBackendId = /* @__PURE__ */ new Map();
12756
+ for (const activity of deps.adapter.getAllActivities()) {
12757
+ if (activity.proplannerId != null) {
12758
+ activityOwnerByBackendId.set(activity.proplannerId, String(activity.id));
12759
+ }
12760
+ }
12761
+ const linkOwnerByBackendId = /* @__PURE__ */ new Map();
12762
+ for (const link of deps.adapter.getAllLinks()) {
12763
+ if (link.proplannerId != null) {
12764
+ linkOwnerByBackendId.set(link.proplannerId, String(link.id));
12765
+ }
12766
+ }
12767
+ assertValidAssignments(
12768
+ activities,
12769
+ "activity",
12770
+ (id) => {
12771
+ const activity = deps.adapter.getActivity(id);
12772
+ return {
12773
+ exists: activity !== null,
12774
+ current: activity?.proplannerId ?? null
12775
+ };
12776
+ },
12777
+ activityOwnerByBackendId
12778
+ );
12779
+ assertValidAssignments(
12780
+ links,
12781
+ "link",
12782
+ (id) => {
12783
+ const link = deps.adapter.getLink(id);
12784
+ return { exists: link !== null, current: link?.proplannerId ?? null };
12785
+ },
12786
+ linkOwnerByBackendId
12787
+ );
12788
+ const activityChanges = [];
12789
+ const linkChanges = [];
12790
+ deps.adapter.batchUpdate(() => {
12791
+ for (const assignment of activities) {
12792
+ const before = deps.adapter.getActivity(assignment.id);
12793
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12794
+ deps.adapter.setActivityField(
12795
+ assignment.id,
12796
+ "proplannerId",
12797
+ assignment.proplannerId
12798
+ );
12799
+ activityChanges.push({
12800
+ id: assignment.id,
12801
+ kind: "updated",
12802
+ fields: {
12803
+ proplannerId: {
12804
+ before: before.proplannerId,
12805
+ after: assignment.proplannerId
12806
+ }
12807
+ },
12808
+ after: { ...before, proplannerId: assignment.proplannerId }
12809
+ });
12810
+ }
12811
+ for (const assignment of links) {
12812
+ const before = deps.adapter.getLink(assignment.id);
12813
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12814
+ deps.adapter.setLinkProplannerId(assignment.id, assignment.proplannerId);
12815
+ linkChanges.push({
12816
+ id: assignment.id,
12817
+ kind: "updated",
12818
+ fields: {
12819
+ proplannerId: {
12820
+ before: before.proplannerId,
12821
+ after: assignment.proplannerId
12822
+ }
12823
+ },
12824
+ after: { ...before, proplannerId: assignment.proplannerId }
12825
+ });
12826
+ }
12827
+ });
12828
+ return {
12829
+ ok: true,
12830
+ changes: {
12831
+ source: action,
12832
+ activities: activityChanges,
12833
+ links: linkChanges,
12834
+ calendars: [],
12835
+ trackingEvents: []
12836
+ }
12837
+ };
12838
+ }
12839
+
12840
+ // src/baselines/normalize-baseline-point.ts
12841
+ function parseBaselineDate(value) {
12842
+ return typeof value === "string" && value.length > 0 ? parseBackendDate(value) : value instanceof Date ? value : null;
12843
+ }
12844
+ function parseFiniteNonNegative(value) {
12845
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
12846
+ if (!Number.isFinite(numeric) || numeric < 0) return null;
12847
+ return numeric;
12848
+ }
12849
+ function normalizeBaselinePoint(point, reporter) {
12850
+ const durationDays = parseFiniteNonNegative(point.duration);
12851
+ const cost = parseFiniteNonNegative(point.cost);
12852
+ const workHours = parseFiniteNonNegative(point.hh_work);
12853
+ if (durationDays === null || cost === null || workHours === null) {
12854
+ reporter.warn(
12855
+ `[baseline] dropping point with non-finite/negative magnitude \u2014 duration=${JSON.stringify(point.duration)}, cost=${JSON.stringify(point.cost)}, hh_work=${JSON.stringify(point.hh_work)}`
12856
+ );
12857
+ return null;
12858
+ }
12859
+ return {
12860
+ startDate: parseBaselineDate(point.start_date),
12861
+ endDate: parseBaselineDate(point.end_date),
12862
+ durationDays,
12863
+ cost,
12864
+ workHours,
12865
+ versionId: typeof point.sectorbaselineversionId === "number" ? point.sectorbaselineversionId : null,
12866
+ isActiveVersion: Boolean(point.sectorbaselineversion?.active),
12867
+ isVisibleVersion: Boolean(point.sectorbaselineversion?.visible),
12868
+ baseCalendarId: point.baseCalendarId != null ? String(point.baseCalendarId) : null,
12869
+ hoursPerDay: typeof point.hoursPerDay === "number" ? point.hoursPerDay : null,
12870
+ hoursPerWeek: typeof point.hoursPerWeek === "number" ? point.hoursPerWeek : null
12871
+ };
12872
+ }
12873
+
12874
+ // src/baselines/apply-baseline-points.ts
12875
+ function applyBaselinePoints(adapter, points, reporter) {
12876
+ const idByProplanner = /* @__PURE__ */ new Map();
12877
+ adapter.forEachActivity((a, id) => {
12878
+ const pid = a.proplannerId;
12879
+ if (typeof pid === "number") idByProplanner.set(pid, id);
12880
+ });
12881
+ const grouped = /* @__PURE__ */ new Map();
12882
+ for (const p of points) {
12883
+ const activityId = p.activityId;
12884
+ if (typeof activityId !== "number") continue;
12885
+ const coreId = idByProplanner.get(activityId);
12886
+ if (coreId === void 0) continue;
12887
+ const arr = grouped.get(coreId);
12888
+ if (arr) arr.push(p);
12889
+ else grouped.set(coreId, [p]);
12890
+ }
12891
+ const changed = [];
12892
+ for (const [coreId, group] of grouped) {
12893
+ adapter.setActivityField(
12894
+ coreId,
12895
+ "baselinePoints",
12896
+ group.map((point) => normalizeBaselinePoint(point, reporter)).filter((point) => point !== null)
12897
+ );
12898
+ changed.push(coreId);
12899
+ }
12900
+ adapter.forEachActivity((a, id) => {
12901
+ if (grouped.has(id)) return;
12902
+ const existing = a.baselinePoints;
12903
+ if (Array.isArray(existing) && existing.length > 0) {
12904
+ adapter.setActivityField(id, "baselinePoints", []);
12905
+ changed.push(id);
12906
+ }
12907
+ });
12908
+ return changed;
12909
+ }
12910
+
12911
+ // src/baselines/baseline-snapshot.ts
12912
+ function buildBaselineSnapshot(activity) {
12913
+ const active = getActiveBaseline(activity);
12914
+ if (!active) return null;
12915
+ return {
12916
+ startDate: active.startDate,
12917
+ endDate: active.endDate,
12918
+ durationDays: active.durationDays,
12919
+ cost: active.cost,
12920
+ workHours: active.workHours
12921
+ };
12922
+ }
12923
+ function emitBaselineSnapshotColumns(state) {
12924
+ const changed = [];
12925
+ state.forEachActivity((activity, id) => {
12926
+ const snapshot = buildBaselineSnapshot(activity);
12927
+ if (!snapshot) {
12928
+ if (activity.baselineSnapshot !== null) {
12929
+ state.setActivityField(id, "baselineSnapshot", null);
12930
+ changed.push(id);
12931
+ }
12932
+ return;
12933
+ }
12934
+ state.setActivityField(id, "baselineSnapshot", snapshot);
12935
+ changed.push(id);
12936
+ });
12937
+ return changed;
12938
+ }
12939
+
12940
+ // src/baselines/apply-baseline-overlay.ts
12941
+ function applyBaselineOverlay(state, points, now, reporter, defaultBaseCalendarId = null) {
12942
+ const changed = /* @__PURE__ */ new Set();
12943
+ for (const id of applyBaselinePoints(
12944
+ state,
12945
+ points,
12946
+ reporter
12947
+ )) {
12948
+ changed.add(id);
12949
+ }
12950
+ if (now) {
12951
+ for (const id of runExpectedProgressBase(state, now, defaultBaseCalendarId))
12952
+ changed.add(id);
12953
+ }
12954
+ for (const id of emitBaselineSnapshotColumns(state)) changed.add(id);
12955
+ for (const id of emitRealCost(state)) changed.add(id);
12956
+ return [...changed];
12957
+ }
12958
+
12959
+ // src/propagations/downward/recompute-ponderator-for-parent.ts
12960
+ function recomputePonderatorsForParent(parentId, criterion, adapter) {
12961
+ if (!parentId || parentId === "0") return;
12962
+ const childIds = adapter.getChildren(parentId);
12963
+ if (childIds.length === 0) return;
12964
+ const pesoById = /* @__PURE__ */ new Map();
12965
+ let denominator = 0;
12966
+ for (const childId of childIds) {
12967
+ const peso = criterionValue(String(childId), criterion, adapter);
12968
+ pesoById.set(String(childId), peso);
12969
+ denominator += peso;
12970
+ }
12971
+ const relationValue = denominator ? 100 / denominator : 0;
12972
+ for (const childId of childIds) {
12973
+ const child = adapter.getActivity(childId);
12974
+ if (!child) continue;
12975
+ const points = child.baselinePoints;
12976
+ const active = getActiveBaseline(child);
12977
+ if (active) {
12978
+ const peso = pesoById.get(String(childId)) ?? 0;
12979
+ adapter.setActivityField(
12980
+ childId,
12981
+ COLUMN.PONDERATOR,
12982
+ relationValue * peso
12983
+ );
12984
+ } else if (!Array.isArray(points) || points.length === 0) {
12985
+ adapter.setActivityField(childId, COLUMN.PONDERATOR, 0);
12986
+ }
12987
+ }
12988
+ }
12989
+ function criterionValue(childId, criterion, adapter) {
12990
+ const child = adapter.getActivity(childId);
12991
+ if (!child) return 0;
12992
+ const active = getActiveBaseline(child);
12993
+ if (!active) return 0;
12994
+ if (criterion === "COST") return toNumber(active.cost);
12995
+ if (criterion === "HH") return toNumber(active.workHours);
12996
+ const type = child.type;
12997
+ if (type === "project")
12998
+ return sumRecursiveBaselineDurationDays(childId, adapter);
12999
+ if (type === "milestone") return 0;
13000
+ return toNumber(active.durationDays);
13001
+ }
13002
+ function sumRecursiveBaselineDurationDays(activityId, adapter) {
13003
+ let sum = 0;
13004
+ for (const childId of adapter.getChildren(activityId)) {
13005
+ const child = adapter.getActivity(childId);
13006
+ if (!child) continue;
13007
+ const active = getActiveBaseline(child);
13008
+ if (!active) continue;
13009
+ const type = child.type;
13010
+ if (type === "project") {
13011
+ sum += sumRecursiveBaselineDurationDays(String(childId), adapter);
13012
+ } else if (type !== "milestone") {
13013
+ sum += toNumber(active.durationDays);
13014
+ }
13015
+ }
13016
+ return sum;
13017
+ }
13018
+ function toNumber(v) {
13019
+ const n = typeof v === "string" ? parseFloat(v) : v;
13020
+ return typeof n === "number" && Number.isFinite(n) ? n : 0;
13021
+ }
13022
+
13023
+ // src/propagations/downward/recompute-all-ponderators.ts
13024
+ function recomputeAllPonderators(criterion, adapter) {
13025
+ adapter.forEachActivity((_a, id) => {
13026
+ adapter.setActivityField(id, COLUMN.PONDERATOR, 0);
13027
+ });
13028
+ adapter.forEachActivity((_a, id) => {
13029
+ if (adapter.getChildren(id).length > 0) {
13030
+ recomputePonderatorsForParent(String(id), criterion, adapter);
13031
+ }
13032
+ });
13033
+ }
13034
+
13035
+ // src/propagations/upward/progress-rollup.ts
13036
+ function recomputeAllProgressRollup(adapter) {
13037
+ for (const parentId of parentsDeepestFirst(adapter)) {
13038
+ rollupParent(parentId, adapter);
13039
+ }
13040
+ }
13041
+ function parentsDeepestFirst(adapter) {
13042
+ const parents = /* @__PURE__ */ new Set();
13043
+ adapter.forEachActivity((_activity, id) => {
13044
+ if (adapter.getChildren(id).length > 0) parents.add(id);
13045
+ });
13046
+ const depthById = /* @__PURE__ */ new Map();
13047
+ for (const id of parents) depthById.set(id, depthOf2(id, adapter));
13048
+ return [...parents].sort(
13049
+ (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
13050
+ );
13051
+ }
13052
+ function depthOf2(id, adapter) {
13053
+ let depth = 0;
13054
+ let current = id;
13055
+ while (current && current !== "0") {
13056
+ if (!adapter.getActivity(current)) break;
13057
+ depth += 1;
13058
+ current = adapter.getParentId(current);
13059
+ }
13060
+ return depth;
13061
+ }
13062
+ function rollupParent(parentId, adapter) {
13063
+ const childIds = adapter.getChildren(parentId);
13064
+ if (childIds.length === 0) return;
13065
+ const children = [];
13066
+ for (const childId of childIds) {
13067
+ const child = adapter.getActivity(childId);
13068
+ if (!child) continue;
13069
+ children.push({
13070
+ progress: Number(child.progress ?? 0),
13071
+ ponderator: Number(child.ponderator ?? 0)
13072
+ });
13073
+ }
13074
+ const rollup = computeWeightedProgressRollup(children);
13075
+ if (rollup !== null) {
13076
+ adapter.setActivityField(
13077
+ parentId,
13078
+ COLUMN.PROGRESS,
13079
+ roundProgressPerLevel(rollup)
13080
+ );
13081
+ }
13082
+ }
13083
+
13084
+ // src/dispatch/shared/baseline-weighted-fields.ts
13085
+ function recomputeBaselineWeightedFields(deps, criterion) {
13086
+ recomputeAllPonderators(criterion, deps.adapter);
13087
+ recomputeAllProgressRollup(deps.adapter);
13088
+ if (deps.now) {
13089
+ runExpectedProgressBase(
13090
+ deps.adapter,
13091
+ deps.now,
13092
+ deps.defaultBaseCalendarId ?? null
13093
+ );
13094
+ applyExpectedProgressLive(deps.adapter, deps.now);
13095
+ }
13096
+ emitRealCost(deps.adapter);
13097
+ if (deps.now) applyStatusPass(deps.adapter, deps.sector.statusCriteria);
13098
+ }
13099
+
13100
+ // src/dispatch/handlers/baseline-apply.ts
13101
+ function activeBaselineSignature(deps) {
13102
+ return JSON.stringify(
13103
+ deps.adapter.getAllIds().map((id) => {
13104
+ const point = deps.adapter.getActivity(id)?.baselinePoints.find((candidate) => candidate.isActiveVersion);
13105
+ return point ? [
13106
+ String(id),
13107
+ point.versionId,
13108
+ point.startDate?.getTime() ?? null,
13109
+ point.endDate?.getTime() ?? null,
13110
+ point.durationDays,
13111
+ point.cost,
13112
+ point.workHours,
13113
+ point.baseCalendarId,
13114
+ point.hoursPerDay,
13115
+ point.hoursPerWeek
13116
+ ] : [String(id), null];
13117
+ })
13118
+ );
13119
+ }
13120
+ function dispatchBaselineApply(action, deps) {
13121
+ const beforeSnap = snapshotActivities(
13122
+ deps.adapter,
13123
+ new Set(deps.adapter.getAllIds())
13124
+ );
13125
+ const activeBaselineBefore = activeBaselineSignature(deps);
13126
+ const changedIds = applyBaselineOverlay(
13127
+ deps.adapter,
13128
+ action.points,
13129
+ deps.now,
13130
+ deps.reporter,
13131
+ deps.defaultBaseCalendarId ?? null
13132
+ );
13133
+ if (activeBaselineSignature(deps) !== activeBaselineBefore) {
13134
+ recomputeBaselineWeightedFields(
13135
+ deps,
13136
+ deps.sector.activityCreter ?? "DURATION"
13137
+ );
13138
+ }
13139
+ return {
13140
+ ok: true,
13141
+ changes: assembleChangeSet(deps.adapter, {
13142
+ source: action,
13143
+ beforeSnap,
13144
+ touchedIds: changedIds,
13145
+ scheduledIds: [],
13146
+ hoursPerDay: deps.sector.hoursPerDay
13147
+ })
13148
+ };
13149
+ }
13150
+
13151
+ // src/dispatch/handlers/ponderator-criterion-set.ts
13152
+ function assertValidCriterion(value) {
13153
+ if (value === "DURATION" || value === "COST" || value === "HH") return;
13154
+ throw new Error(
13155
+ `ponderator-criterion-set: invalid criterion "${String(value)}"`
13156
+ );
13157
+ }
13158
+ function dispatchPonderatorCriterionSet(action, deps) {
13159
+ assertValidCriterion(action.criterion);
13160
+ const allIds = new Set(deps.adapter.getAllIds());
13161
+ const beforeSnap = snapshotActivities(deps.adapter, allIds);
13162
+ recomputeBaselineWeightedFields(deps, action.criterion);
13163
+ deps.sector.activityCreter = action.criterion;
13164
+ return {
13165
+ ok: true,
13166
+ changes: assembleChangeSet(deps.adapter, {
13167
+ source: action,
13168
+ beforeSnap,
13169
+ touchedIds: allIds,
13170
+ scheduledIds: [],
13171
+ hoursPerDay: deps.sector.hoursPerDay
13172
+ })
13173
+ };
13174
+ }
13175
+
13176
+ // src/dispatch/handlers/status-criteria-set.ts
13177
+ function dispatchStatusCriteriaSet(action, deps) {
13178
+ const resolved = resolveStatusCriteria(action.criteria);
13179
+ const allIds = new Set(deps.adapter.getAllIds());
13180
+ const beforeSnap = snapshotActivities(deps.adapter, allIds);
13181
+ const changedIds = applyStatusPass(deps.adapter, resolved);
13182
+ deps.sector.statusCriteria = resolved;
13183
+ return {
13184
+ ok: true,
13185
+ changes: assembleChangeSet(deps.adapter, {
13186
+ source: action,
13187
+ beforeSnap,
13188
+ touchedIds: changedIds,
13189
+ scheduledIds: [],
13190
+ hoursPerDay: deps.sector.hoursPerDay
13191
+ })
13192
+ };
13193
+ }
13194
+
12561
13195
  // src/dispatch/dispatch.ts
12562
13196
  async function dispatch(action, options, deps) {
13197
+ if (action.kind === "persistence-acknowledge") {
13198
+ return dispatchPersistenceAcknowledge(action, deps);
13199
+ }
13200
+ if (action.kind === "baseline-apply") {
13201
+ return dispatchBaselineApply(action, deps);
13202
+ }
13203
+ if (action.kind === "ponderator-criterion-set") {
13204
+ return dispatchPonderatorCriterionSet(action, deps);
13205
+ }
13206
+ if (action.kind === "status-criteria-set") {
13207
+ return dispatchStatusCriteriaSet(action, deps);
13208
+ }
12563
13209
  if (action.kind === "link-create" || action.kind === "link-update" || action.kind === "link-delete") {
12564
13210
  return dispatchLink(action, options, deps);
12565
13211
  }
@@ -15515,8 +16161,15 @@ var WriteCapture = class {
15515
16161
  /** Pre-applyResults clone of an activity the autoscheduler will move (first-wins). */
15516
16162
  noteScheduledBefore(activityId, current) {
15517
16163
  const journal = this._journal;
15518
- if (!journal || journal.scheduledBefore.has(activityId)) return;
15519
- journal.scheduledBefore.set(activityId, cloneCoreActivity(current));
16164
+ if (!journal) return;
16165
+ const beforeImage = cloneCoreActivity(current);
16166
+ if (!journal.scheduledBefore.has(activityId)) {
16167
+ journal.scheduledBefore.set(activityId, beforeImage);
16168
+ }
16169
+ if (!journal.before.has(activityId)) {
16170
+ journal.before.set(activityId, beforeImage);
16171
+ }
16172
+ journal.dirty.add(activityId);
15520
16173
  }
15521
16174
  /** Field write on a link — pre-mutation clone on first write per link id. */
15522
16175
  noteLinkField(linkId, current) {
@@ -15530,20 +16183,6 @@ var WriteCapture = class {
15530
16183
  });
15531
16184
  }
15532
16185
  };
15533
- function cloneCoreActivity(activity) {
15534
- return {
15535
- ...activity,
15536
- startDate: new Date(activity.startDate),
15537
- endDate: new Date(activity.endDate),
15538
- constraintDate: activity.constraintDate ? new Date(activity.constraintDate) : null,
15539
- dateOrigin: activity.dateOrigin ? new Date(activity.dateOrigin) : null,
15540
- newActivityIds: [...activity.newActivityIds],
15541
- pendingRequestIds: [...activity.pendingRequestIds],
15542
- responsableIds: [...activity.responsableIds],
15543
- tagIds: [...activity.tagIds],
15544
- baselinePoints: [...activity.baselinePoints]
15545
- };
15546
- }
15547
16186
  function cloneLink(link) {
15548
16187
  return { ...link };
15549
16188
  }
@@ -15723,6 +16362,18 @@ var HierarchyIndex = class {
15723
16362
  arr.push(activity.id);
15724
16363
  this._childrenByParent.set(key, arr);
15725
16364
  }
16365
+ const compareIds = (left, right) => {
16366
+ const leftActivity = this.activities.get(left);
16367
+ const rightActivity = this.activities.get(right);
16368
+ if (!(leftActivity && rightActivity)) {
16369
+ return String(left).localeCompare(String(right));
16370
+ }
16371
+ return compareActivitiesInVisualOrder(leftActivity, rightActivity);
16372
+ };
16373
+ this._roots.sort(compareIds);
16374
+ for (const children of this._childrenByParent.values()) {
16375
+ children.sort(compareIds);
16376
+ }
15726
16377
  }
15727
16378
  };
15728
16379
 
@@ -15785,6 +16436,13 @@ var ScheduleState = class {
15785
16436
  // whether a soft constraint repositioned the activity. Never persisted,
15786
16437
  // never in the ChangeSet — not domain data on CoreActivity.
15787
16438
  _lastStartDate = /* @__PURE__ */ new Map();
16439
+ // Transient gesture state: what a leaf looked like right before it gained its
16440
+ // first child and became a summary. Read by the demotion to give the activity
16441
+ // its own values back instead of the aggregate its children left behind. Not
16442
+ // persisted and not in the ChangeSet: it only makes sense inside the session
16443
+ // that performed the promotion. Kept (not consumed) so undo/redo of a
16444
+ // demotion restores identically every time.
16445
+ _promotionSnapshot = /* @__PURE__ */ new Map();
15788
16446
  _links = /* @__PURE__ */ new Map();
15789
16447
  _outgoing = /* @__PURE__ */ new Map();
15790
16448
  _incoming = /* @__PURE__ */ new Map();
@@ -15800,6 +16458,7 @@ var ScheduleState = class {
15800
16458
  _viewState = new ViewStateStore();
15801
16459
  _viewStateBefore = null;
15802
16460
  _lastStartDateBefore = null;
16461
+ _promotionSnapshotBefore = null;
15803
16462
  // Calendar arithmetic + in-dispatch memoization. Assigned in the
15804
16463
  // constructor once the calendar reader is built. See `CalendarCalculator`.
15805
16464
  _calendar;
@@ -15884,6 +16543,19 @@ var ScheduleState = class {
15884
16543
  this._captureLastStartDateOnce();
15885
16544
  this._lastStartDate.set(String(activityId), startDate);
15886
16545
  }
16546
+ getPromotionSnapshot(activityId) {
16547
+ return this._promotionSnapshot.get(String(activityId)) ?? null;
16548
+ }
16549
+ setPromotionSnapshot(activityId, snapshot) {
16550
+ this._capturePromotionSnapshotOnce();
16551
+ this._promotionSnapshot.set(String(activityId), snapshot);
16552
+ }
16553
+ _capturePromotionSnapshotOnce() {
16554
+ if (this._writeCapture.peek() === null) return;
16555
+ if (this._promotionSnapshotBefore === null) {
16556
+ this._promotionSnapshotBefore = new Map(this._promotionSnapshot);
16557
+ }
16558
+ }
15887
16559
  _captureLastStartDateOnce() {
15888
16560
  if (this._writeCapture.peek() === null) return;
15889
16561
  if (this._lastStartDateBefore === null) {
@@ -16000,6 +16672,7 @@ var ScheduleState = class {
16000
16672
  this._writeCapture.begin();
16001
16673
  this._viewStateBefore = null;
16002
16674
  this._lastStartDateBefore = null;
16675
+ this._promotionSnapshotBefore = null;
16003
16676
  }
16004
16677
  /** The live journal, or null when no capture is active. */
16005
16678
  peekWriteCapture() {
@@ -16010,6 +16683,7 @@ var ScheduleState = class {
16010
16683
  this._writeCapture.end();
16011
16684
  this._viewStateBefore = null;
16012
16685
  this._lastStartDateBefore = null;
16686
+ this._promotionSnapshotBefore = null;
16013
16687
  }
16014
16688
  // -- Replay-specific mutators --------------------------------------------
16015
16689
  setActivityField(activityId, field, value) {
@@ -16161,439 +16835,156 @@ var ScheduleState = class {
16161
16835
  if (this._viewStateBefore !== null) {
16162
16836
  this._viewState.restore(this._viewStateBefore);
16163
16837
  }
16838
+ if (this._promotionSnapshotBefore !== null) {
16839
+ this._promotionSnapshot = this._promotionSnapshotBefore;
16840
+ }
16164
16841
  if (this._lastStartDateBefore !== null) {
16165
16842
  this._lastStartDate = this._lastStartDateBefore;
16166
16843
  }
16167
16844
  this._cache.ids = null;
16168
- this._cache.visualOrderIds = null;
16169
- this._hierarchy.markDirty();
16170
- }
16171
- _restoreCorrelatives(correlativeBefore) {
16172
- for (const [id, before] of correlativeBefore) {
16173
- const live = this._activities.get(id);
16174
- if (live) live.correlativeId = before;
16175
- }
16176
- }
16177
- _undoLinkAdds(ops) {
16178
- for (const op of ops)
16179
- if (op.kind === "link-add") this._detachLink(op.linkId);
16180
- }
16181
- _undoInserts(ops) {
16182
- for (const op of ops)
16183
- if (op.kind === "insert") this._activities.delete(op.activityId);
16184
- }
16185
- _reInsertRemovedActivities(ops) {
16186
- for (const op of ops)
16187
- if (op.kind === "remove") this._activities.set(op.activityId, op.before);
16188
- }
16189
- _reAddRemovedLinks(ops) {
16190
- for (const op of ops)
16191
- if (op.kind === "link-remove") this._attachLink(op.before);
16192
- }
16193
- _restoreLinkFields(ops) {
16194
- for (const op of ops)
16195
- if (op.kind === "link-field" && this._links.has(op.linkId))
16196
- this._links.set(op.linkId, op.before);
16197
- }
16198
- _restoreActivityFields(before) {
16199
- for (const [id, snapshot] of before) {
16200
- const live = this._activities.get(id);
16201
- if (live) this._restoreScalarFields(live, snapshot);
16202
- }
16203
- }
16204
- _restoreScalarFields(live, before) {
16205
- for (const key of Object.keys(live)) {
16206
- if (!Object.hasOwn(before, key)) Reflect.deleteProperty(live, key);
16207
- }
16208
- for (const key of Object.keys(before)) {
16209
- Reflect.set(live, key, Reflect.get(before, key));
16210
- }
16211
- }
16212
- _attachLink(link) {
16213
- this._links.set(String(link.id), { ...link });
16214
- this._indexLink(link);
16215
- }
16216
- _detachLink(linkId) {
16217
- const link = this.getLink(linkId);
16218
- if (!link) return;
16219
- this._links.delete(String(linkId));
16220
- this._deindexLink(link);
16221
- }
16222
- };
16223
- var EMPTY_LINK_IDS = [];
16224
- function isMutableLinkField(field) {
16225
- return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE;
16226
- }
16227
- function fieldAffectsVisualOrder(field) {
16228
- return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
16229
- }
16230
- function detachLinkRef(refs, linkId) {
16231
- return refs.filter((ref) => String(ref) !== String(linkId));
16232
- }
16233
- function pushLinkRef(map, key, linkId) {
16234
- const refs = map.get(key);
16235
- if (refs) refs.push(linkId);
16236
- else map.set(key, [linkId]);
16237
- }
16238
- function buildFlags(snapshot) {
16239
- const rawFlags = snapshot.runtimeFlags ?? {};
16240
- return {
16241
- isPasting: Boolean(rawFlags.isPasting),
16242
- isBulkOperation: Boolean(rawFlags.isBulkOperation),
16243
- dragWithMultiple: Boolean(rawFlags.dragWithMultiple),
16244
- draggingSingleMilestone: Boolean(rawFlags.draggingSingleMilestone),
16245
- creatingActivity: false,
16246
- avoidAutoScheduleBeforeDrag: false,
16247
- maxPerformance: Boolean(rawFlags.maxPerformance),
16248
- allCheckedTaskIds: []
16249
- };
16250
- }
16251
-
16252
- // src/init/read-api.ts
16253
- function readActivity(state, id) {
16254
- return state.getActivity(id);
16255
- }
16256
- function readAllActivities(state) {
16257
- return state.getAllActivities();
16258
- }
16259
- function readAllIds(state) {
16260
- return state.getAllIds().map(String);
16261
- }
16262
- function forEachActivityId(state, visit) {
16263
- state.forEachActivity((_activity, id) => visit(id));
16264
- }
16265
- function readChildren(state, parentId) {
16266
- const childIds = parentId === ROOT_PARENT_ID ? readChildrenIds(state, ROOT_PARENT_ID) : state.getChildren(parentId);
16267
- const out = [];
16268
- for (const id of childIds) {
16269
- const snap = state.getActivity(id);
16270
- if (snap) out.push(snap);
16271
- }
16272
- out.sort((a, b) => (a.correlativeId ?? 0) - (b.correlativeId ?? 0));
16273
- return out;
16274
- }
16275
- function readChildrenIds(state, parentId) {
16276
- const key = parentId;
16277
- if (key === ROOT_PARENT_ID) {
16278
- const roots = [];
16279
- state.forEachActivity((activity, id) => {
16280
- if (isRootParent(activity.parentId)) roots.push(id);
16281
- });
16282
- return roots;
16283
- }
16284
- return [...state.getChildren(key)].map(String);
16285
- }
16286
- function readSelectedActivityIds(state) {
16287
- return state.checkedIds().map(String);
16288
- }
16289
- function readHasChild(state, parentId) {
16290
- return state.getChildren(parentId).length > 0;
16291
- }
16292
- function readLink(state, id) {
16293
- const l = state.getLink(id);
16294
- return l ? snapshotToLink(l) : null;
16295
- }
16296
- function readAllLinks(state) {
16297
- return state.getAllLinks().map((l) => snapshotToLink(l));
16298
- }
16299
-
16300
- // src/propagations/downward/recompute-ponderator-for-parent.ts
16301
- function recomputePonderatorsForParent(parentId, criterion, adapter) {
16302
- if (!parentId || parentId === "0") return;
16303
- const childIds = adapter.getChildren(parentId);
16304
- if (childIds.length === 0) return;
16305
- const pesoById = /* @__PURE__ */ new Map();
16306
- let denominator = 0;
16307
- for (const childId of childIds) {
16308
- const peso = criterionValue(String(childId), criterion, adapter);
16309
- pesoById.set(String(childId), peso);
16310
- denominator += peso;
16311
- }
16312
- const relationValue = denominator ? 100 / denominator : 0;
16313
- for (const childId of childIds) {
16314
- const child = adapter.getActivity(childId);
16315
- if (!child) continue;
16316
- const points = child.baselinePoints;
16317
- const active = getActiveBaseline(child);
16318
- if (active) {
16319
- const peso = pesoById.get(String(childId)) ?? 0;
16320
- adapter.setActivityField(
16321
- childId,
16322
- COLUMN.PONDERATOR,
16323
- relationValue * peso
16324
- );
16325
- } else if (!Array.isArray(points) || points.length === 0) {
16326
- adapter.setActivityField(childId, COLUMN.PONDERATOR, 0);
16327
- }
16328
- }
16329
- }
16330
- function criterionValue(childId, criterion, adapter) {
16331
- const child = adapter.getActivity(childId);
16332
- if (!child) return 0;
16333
- const active = getActiveBaseline(child);
16334
- if (!active) return 0;
16335
- if (criterion === "COST") return toNumber(active.cost);
16336
- if (criterion === "HH") return toNumber(active.workHours);
16337
- const type = child.type;
16338
- if (type === "project")
16339
- return sumRecursiveBaselineDurationDays(childId, adapter);
16340
- if (type === "milestone") return 0;
16341
- return toNumber(active.durationDays);
16342
- }
16343
- function sumRecursiveBaselineDurationDays(activityId, adapter) {
16344
- let sum = 0;
16345
- for (const childId of adapter.getChildren(activityId)) {
16346
- const child = adapter.getActivity(childId);
16347
- if (!child) continue;
16348
- const active = getActiveBaseline(child);
16349
- if (!active) continue;
16350
- const type = child.type;
16351
- if (type === "project") {
16352
- sum += sumRecursiveBaselineDurationDays(String(childId), adapter);
16353
- } else if (type !== "milestone") {
16354
- sum += toNumber(active.durationDays);
16355
- }
16356
- }
16357
- return sum;
16358
- }
16359
- function toNumber(v) {
16360
- const n = typeof v === "string" ? parseFloat(v) : v;
16361
- return typeof n === "number" && Number.isFinite(n) ? n : 0;
16362
- }
16363
-
16364
- // src/propagations/downward/recompute-all-ponderators.ts
16365
- function recomputeAllPonderators(criterion, adapter) {
16366
- adapter.forEachActivity((_a, id) => {
16367
- adapter.setActivityField(id, COLUMN.PONDERATOR, 0);
16368
- });
16369
- adapter.forEachActivity((_a, id) => {
16370
- if (adapter.getChildren(id).length > 0) {
16371
- recomputePonderatorsForParent(String(id), criterion, adapter);
16845
+ this._cache.visualOrderIds = null;
16846
+ this._hierarchy.markDirty();
16847
+ }
16848
+ _restoreCorrelatives(correlativeBefore) {
16849
+ for (const [id, before] of correlativeBefore) {
16850
+ const live = this._activities.get(id);
16851
+ if (live) live.correlativeId = before;
16372
16852
  }
16373
- });
16374
- }
16375
-
16376
- // src/propagations/upward/progress-rollup.ts
16377
- function recomputeAllProgressRollup(adapter) {
16378
- for (const parentId of parentsDeepestFirst(adapter)) {
16379
- rollupParent(parentId, adapter);
16380
16853
  }
16381
- }
16382
- function parentsDeepestFirst(adapter) {
16383
- const parents = /* @__PURE__ */ new Set();
16384
- adapter.forEachActivity((_activity, id) => {
16385
- if (adapter.getChildren(id).length > 0) parents.add(id);
16386
- });
16387
- const depthById = /* @__PURE__ */ new Map();
16388
- for (const id of parents) depthById.set(id, depthOf2(id, adapter));
16389
- return [...parents].sort(
16390
- (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
16391
- );
16392
- }
16393
- function depthOf2(id, adapter) {
16394
- let depth = 0;
16395
- let current = id;
16396
- while (current && current !== "0") {
16397
- if (!adapter.getActivity(current)) break;
16398
- depth += 1;
16399
- current = adapter.getParentId(current);
16854
+ _undoLinkAdds(ops) {
16855
+ for (const op of ops)
16856
+ if (op.kind === "link-add") this._detachLink(op.linkId);
16400
16857
  }
16401
- return depth;
16402
- }
16403
- function rollupParent(parentId, adapter) {
16404
- const childIds = adapter.getChildren(parentId);
16405
- if (childIds.length === 0) return;
16406
- const children = [];
16407
- for (const childId of childIds) {
16408
- const child = adapter.getActivity(childId);
16409
- if (!child) continue;
16410
- children.push({
16411
- progress: Number(child.progress ?? 0),
16412
- ponderator: Number(child.ponderator ?? 0)
16413
- });
16858
+ _undoInserts(ops) {
16859
+ for (const op of ops)
16860
+ if (op.kind === "insert") this._activities.delete(op.activityId);
16414
16861
  }
16415
- const rollup = computeWeightedProgressRollup(children);
16416
- if (rollup !== null) {
16417
- adapter.setActivityField(
16418
- parentId,
16419
- COLUMN.PROGRESS,
16420
- roundProgressPerLevel(rollup)
16421
- );
16862
+ _reInsertRemovedActivities(ops) {
16863
+ for (const op of ops)
16864
+ if (op.kind === "remove") this._activities.set(op.activityId, op.before);
16422
16865
  }
16423
- }
16424
-
16425
- // src/baselines/normalize-baseline-point.ts
16426
- function parseBaselineDate(value) {
16427
- return typeof value === "string" && value.length > 0 ? parseBackendDate(value) : value instanceof Date ? value : null;
16428
- }
16429
- function parseFiniteNonNegative(value) {
16430
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
16431
- if (!Number.isFinite(numeric) || numeric < 0) return null;
16432
- return numeric;
16433
- }
16434
- function normalizeBaselinePoint(point, reporter) {
16435
- const durationDays = parseFiniteNonNegative(point.duration);
16436
- const cost = parseFiniteNonNegative(point.cost);
16437
- const workHours = parseFiniteNonNegative(point.hh_work);
16438
- if (durationDays === null || cost === null || workHours === null) {
16439
- reporter.warn(
16440
- `[baseline] dropping point with non-finite/negative magnitude \u2014 duration=${JSON.stringify(point.duration)}, cost=${JSON.stringify(point.cost)}, hh_work=${JSON.stringify(point.hh_work)}`
16441
- );
16442
- return null;
16866
+ _reAddRemovedLinks(ops) {
16867
+ for (const op of ops)
16868
+ if (op.kind === "link-remove") this._attachLink(op.before);
16443
16869
  }
16444
- return {
16445
- startDate: parseBaselineDate(point.start_date),
16446
- endDate: parseBaselineDate(point.end_date),
16447
- durationDays,
16448
- cost,
16449
- workHours,
16450
- versionId: typeof point.sectorbaselineversionId === "number" ? point.sectorbaselineversionId : null,
16451
- isActiveVersion: Boolean(point.sectorbaselineversion?.active),
16452
- baseCalendarId: point.baseCalendarId != null ? String(point.baseCalendarId) : null,
16453
- hoursPerDay: typeof point.hoursPerDay === "number" ? point.hoursPerDay : null,
16454
- hoursPerWeek: typeof point.hoursPerWeek === "number" ? point.hoursPerWeek : null
16455
- };
16456
- }
16457
-
16458
- // src/baselines/apply-baseline-points.ts
16459
- function applyBaselinePoints(adapter, points, reporter) {
16460
- const idByProplanner = /* @__PURE__ */ new Map();
16461
- adapter.forEachActivity((a, id) => {
16462
- const pid = a.proplannerId;
16463
- if (typeof pid === "number") idByProplanner.set(pid, id);
16464
- });
16465
- const grouped = /* @__PURE__ */ new Map();
16466
- for (const p of points) {
16467
- const activityId = p.activityId;
16468
- if (typeof activityId !== "number") continue;
16469
- const coreId = idByProplanner.get(activityId);
16470
- if (coreId === void 0) continue;
16471
- const arr = grouped.get(coreId);
16472
- if (arr) arr.push(p);
16473
- else grouped.set(coreId, [p]);
16870
+ _restoreLinkFields(ops) {
16871
+ for (const op of ops)
16872
+ if (op.kind === "link-field" && this._links.has(op.linkId))
16873
+ this._links.set(op.linkId, op.before);
16474
16874
  }
16475
- const changed = [];
16476
- for (const [coreId, group] of grouped) {
16477
- adapter.setActivityField(
16478
- coreId,
16479
- "baselinePoints",
16480
- group.map((point) => normalizeBaselinePoint(point, reporter)).filter((point) => point !== null)
16481
- );
16482
- changed.push(coreId);
16875
+ _restoreActivityFields(before) {
16876
+ for (const [id, snapshot] of before) {
16877
+ const live = this._activities.get(id);
16878
+ if (live) this._restoreScalarFields(live, snapshot);
16879
+ }
16483
16880
  }
16484
- adapter.forEachActivity((a, id) => {
16485
- if (grouped.has(id)) return;
16486
- const existing = a.baselinePoints;
16487
- if (Array.isArray(existing) && existing.length > 0) {
16488
- adapter.setActivityField(id, "baselinePoints", []);
16489
- changed.push(id);
16881
+ _restoreScalarFields(live, before) {
16882
+ for (const key of Object.keys(live)) {
16883
+ if (!Object.hasOwn(before, key)) Reflect.deleteProperty(live, key);
16490
16884
  }
16491
- });
16492
- return changed;
16885
+ for (const key of Object.keys(before)) {
16886
+ Reflect.set(live, key, Reflect.get(before, key));
16887
+ }
16888
+ }
16889
+ _attachLink(link) {
16890
+ this._links.set(String(link.id), { ...link });
16891
+ this._indexLink(link);
16892
+ }
16893
+ _detachLink(linkId) {
16894
+ const link = this.getLink(linkId);
16895
+ if (!link) return;
16896
+ this._links.delete(String(linkId));
16897
+ this._deindexLink(link);
16898
+ }
16899
+ };
16900
+ var EMPTY_LINK_IDS = [];
16901
+ function isMutableLinkField(field) {
16902
+ return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE;
16493
16903
  }
16494
-
16495
- // src/baselines/baseline-snapshot.ts
16496
- function buildBaselineSnapshot(activity) {
16497
- const active = getActiveBaseline(activity);
16498
- if (!active) return null;
16904
+ function fieldAffectsVisualOrder(field) {
16905
+ return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
16906
+ }
16907
+ function detachLinkRef(refs, linkId) {
16908
+ return refs.filter((ref) => String(ref) !== String(linkId));
16909
+ }
16910
+ function pushLinkRef(map, key, linkId) {
16911
+ const refs = map.get(key);
16912
+ if (refs) refs.push(linkId);
16913
+ else map.set(key, [linkId]);
16914
+ }
16915
+ function buildFlags(snapshot) {
16916
+ const rawFlags = snapshot.runtimeFlags ?? {};
16499
16917
  return {
16500
- startDate: active.startDate,
16501
- endDate: active.endDate,
16502
- durationDays: active.durationDays,
16503
- cost: active.cost,
16504
- workHours: active.workHours
16918
+ isPasting: Boolean(rawFlags.isPasting),
16919
+ isBulkOperation: Boolean(rawFlags.isBulkOperation),
16920
+ dragWithMultiple: Boolean(rawFlags.dragWithMultiple),
16921
+ draggingSingleMilestone: Boolean(rawFlags.draggingSingleMilestone),
16922
+ creatingActivity: false,
16923
+ avoidAutoScheduleBeforeDrag: false,
16924
+ maxPerformance: Boolean(rawFlags.maxPerformance),
16925
+ allCheckedTaskIds: []
16505
16926
  };
16506
16927
  }
16507
- function emitBaselineSnapshotColumns(state) {
16508
- const changed = [];
16509
- state.forEachActivity((activity, id) => {
16510
- const snapshot = buildBaselineSnapshot(activity);
16511
- if (!snapshot) return;
16512
- state.setActivityField(id, "baselineSnapshot", snapshot);
16513
- changed.push(id);
16514
- });
16515
- return changed;
16516
- }
16517
16928
 
16518
- // src/baselines/apply-baseline-overlay.ts
16519
- function applyBaselineOverlay(state, points, now, reporter, defaultBaseCalendarId = null) {
16520
- if (points.length === 0) return [];
16521
- const changed = /* @__PURE__ */ new Set();
16522
- for (const id of applyBaselinePoints(
16523
- state,
16524
- points,
16525
- reporter
16526
- )) {
16527
- changed.add(id);
16528
- }
16529
- if (now) {
16530
- for (const id of runExpectedProgressBase(state, now, defaultBaseCalendarId))
16531
- changed.add(id);
16532
- }
16533
- for (const id of emitBaselineSnapshotColumns(state)) changed.add(id);
16534
- for (const id of emitRealCost(state)) changed.add(id);
16535
- return [...changed];
16929
+ // src/shared/clone-domain-value.ts
16930
+ function cloneDomainValue(value) {
16931
+ return structuredClone(value);
16536
16932
  }
16537
16933
 
16538
- // src/init/recompute-api.ts
16539
- function readPonderator(a) {
16540
- const v = a.ponderator;
16541
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
16934
+ // src/init/read-api.ts
16935
+ function readActivity(state, id) {
16936
+ const activity = state.getActivity(id);
16937
+ return activity ? cloneDomainValue(activity) : null;
16542
16938
  }
16543
- function readProgress(a) {
16544
- const v = a.progress;
16545
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
16939
+ function readAllActivities(state) {
16940
+ return state.getAllActivities().map(cloneDomainValue);
16546
16941
  }
16547
- function recomputeAllPonderators2(state, _hoursPerDay, criterion) {
16548
- const before = /* @__PURE__ */ new Map();
16549
- state.forEachActivity((a, id) => {
16550
- before.set(id, readPonderator(a));
16551
- });
16552
- recomputeAllPonderators(criterion, state);
16553
- const changed = [];
16554
- state.forEachActivity((a, id) => {
16555
- if (readPonderator(a) !== (before.get(id) ?? 0)) {
16556
- changed.push(structuredCloneActivity(a));
16557
- }
16558
- });
16559
- return changed;
16942
+ function readAllIds(state) {
16943
+ return state.getAllIds().map(String);
16560
16944
  }
16561
- function recomputeAllProgressRollup2(state, _hoursPerDay) {
16562
- const before = /* @__PURE__ */ new Map();
16563
- state.forEachActivity((a, id) => {
16564
- before.set(id, readProgress(a));
16565
- });
16566
- recomputeAllProgressRollup(state);
16567
- const changed = [];
16568
- state.forEachActivity((a, id) => {
16569
- if (readProgress(a) !== (before.get(id) ?? 0)) {
16570
- changed.push(structuredCloneActivity(a));
16571
- }
16572
- });
16573
- return changed;
16945
+ function forEachActivityId(state, visit) {
16946
+ state.forEachActivity((_activity, id) => visit(id));
16574
16947
  }
16575
- function applyBaselines(state, _hoursPerDay, points, now, reporter, defaultBaseCalendarId = null) {
16576
- const changedIds = applyBaselineOverlay(
16577
- state,
16578
- points,
16579
- now,
16580
- reporter,
16581
- defaultBaseCalendarId
16582
- );
16583
- const changed = [];
16584
- for (const id of changedIds) {
16948
+ function readChildren(state, parentId) {
16949
+ const childIds = parentId === ROOT_PARENT_ID ? readChildrenIds(state, ROOT_PARENT_ID) : state.getChildren(parentId);
16950
+ const out = [];
16951
+ for (const id of childIds) {
16585
16952
  const snap = state.getActivity(id);
16586
- if (snap) changed.push(structuredCloneActivity(snap));
16953
+ if (snap) out.push(cloneDomainValue(snap));
16954
+ }
16955
+ out.sort((a, b) => (a.correlativeId ?? 0) - (b.correlativeId ?? 0));
16956
+ return out;
16957
+ }
16958
+ function readChildrenIds(state, parentId) {
16959
+ const key = parentId;
16960
+ if (key === ROOT_PARENT_ID) {
16961
+ const roots = [];
16962
+ state.forEachActivity((activity, id) => {
16963
+ if (isRootParent(activity.parentId)) roots.push(id);
16964
+ });
16965
+ return roots;
16587
16966
  }
16588
- return changed;
16967
+ return [...state.getChildren(key)].map(String);
16589
16968
  }
16590
- function recomputeStatus(state, _hoursPerDay, criteria) {
16591
- const changed = [];
16592
- for (const id of applyStatusPass(state, criteria)) {
16593
- const snap = state.getActivity(id);
16594
- if (snap) changed.push(structuredCloneActivity(snap));
16969
+ function readSelectedActivityIds(state) {
16970
+ return state.checkedIds().map(String);
16971
+ }
16972
+ function readHasChild(state, parentId) {
16973
+ if (parentId === ROOT_PARENT_ID) {
16974
+ let hasRoot = false;
16975
+ state.forEachActivity((activity) => {
16976
+ hasRoot ||= isRootParent(activity.parentId);
16977
+ });
16978
+ return hasRoot;
16595
16979
  }
16596
- return changed;
16980
+ return state.getChildren(parentId).length > 0;
16981
+ }
16982
+ function readLink(state, id) {
16983
+ const l = state.getLink(id);
16984
+ return l ? snapshotToLink(l) : null;
16985
+ }
16986
+ function readAllLinks(state) {
16987
+ return state.getAllLinks().map((l) => snapshotToLink(l));
16597
16988
  }
16598
16989
 
16599
16990
  // src/internal/state/project-work-hours.ts
@@ -16703,8 +17094,12 @@ function mergeCoalesced(top, next) {
16703
17094
  };
16704
17095
  return buildUndoEntry(changeSet);
16705
17096
  }
16706
- function isUndoableAction(action) {
16707
- return action.kind !== "sir-sync" && action.kind !== "activity-lookahead-sync";
17097
+ function getDispatchHistoryPolicy(action) {
17098
+ if (action.kind === "persistence-acknowledge") return "clear-on-success";
17099
+ if (action.kind === "sir-sync" || action.kind === "activity-lookahead-sync" || action.kind === "baseline-apply" || action.kind === "ponderator-criterion-set" || action.kind === "status-criteria-set") {
17100
+ return "skip";
17101
+ }
17102
+ return "record";
16708
17103
  }
16709
17104
  function needsResync(entry) {
16710
17105
  for (const change of entry.changeSet.activities) {
@@ -16724,6 +17119,7 @@ function buildInverseChangeSet(state, entry, side) {
16724
17119
  activities.push({
16725
17120
  id: activityId,
16726
17121
  kind: "updated",
17122
+ fields: historyFields(change.fields, side),
16727
17123
  after: snapshot ? structuredCloneActivity(snapshot) : null
16728
17124
  });
16729
17125
  } else if (change.kind === "deleted") {
@@ -16757,6 +17153,7 @@ function buildInverseChangeSet(state, entry, side) {
16757
17153
  links.push({
16758
17154
  id: linkId,
16759
17155
  kind: "updated",
17156
+ fields: historyFields(change.fields, side),
16760
17157
  after: lk ? snapshotToLink(lk) : null
16761
17158
  });
16762
17159
  } else if (change.kind === "deleted") {
@@ -16796,6 +17193,21 @@ function buildInverseChangeSet(state, entry, side) {
16796
17193
  viewState
16797
17194
  };
16798
17195
  }
17196
+ function historyFields(fields, side) {
17197
+ if (fields == null) return void 0;
17198
+ return Object.fromEntries(
17199
+ Object.entries(fields).map(([field, values]) => [
17200
+ field,
17201
+ side === "before" ? {
17202
+ before: cloneDomainValue(values.after),
17203
+ after: cloneDomainValue(values.before)
17204
+ } : {
17205
+ before: cloneDomainValue(values.before),
17206
+ after: cloneDomainValue(values.after)
17207
+ }
17208
+ ])
17209
+ );
17210
+ }
16799
17211
  function invertViewStateChange(change) {
16800
17212
  return {
16801
17213
  activityId: change.activityId,
@@ -16971,6 +17383,7 @@ var UndoRecorder = class {
16971
17383
  clear() {
16972
17384
  this.undoStack.length = 0;
16973
17385
  this.redoStack.length = 0;
17386
+ this.resetCoalesce();
16974
17387
  }
16975
17388
  };
16976
17389
 
@@ -17333,8 +17746,7 @@ function parseActivity(raw, context) {
17333
17746
  expectedProgress: null,
17334
17747
  expectedProgressBaseline: null,
17335
17748
  status: null,
17336
- criticalPath: null,
17337
- promotionRestore: null
17749
+ criticalPath: null
17338
17750
  };
17339
17751
  }
17340
17752
  function forceRootProjectToRoot(activities) {
@@ -18339,6 +18751,117 @@ function snapDemotedLeafTiming(state, activityId) {
18339
18751
  state.setActivityField(activityId, ACTIVITY_PROPERTY.END_DATE, end);
18340
18752
  }
18341
18753
 
18754
+ // src/propagations/upward/recompute-real-work.ts
18755
+ function recomputeCanonicalRealWork(state) {
18756
+ const rootIds = [];
18757
+ const changedIds = [];
18758
+ state.forEachActivity((activity, activityId) => {
18759
+ if (activity.parentId === null) rootIds.push(String(activityId));
18760
+ });
18761
+ const visit = (activityId) => {
18762
+ const activity = state.getActivity(activityId);
18763
+ if (!activity) return 0;
18764
+ const childIds = [...state.getChildren(activityId)];
18765
+ const realWork = childIds.length === 0 ? computeLeafRealWork(activity.workHours, activity.progress) : childIds.reduce(
18766
+ (total, childId) => total + visit(String(childId)),
18767
+ 0
18768
+ );
18769
+ state.setActivityField(
18770
+ activityId,
18771
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
18772
+ realWork
18773
+ );
18774
+ changedIds.push(activityId);
18775
+ return realWork;
18776
+ };
18777
+ for (const rootId of rootIds) visit(rootId);
18778
+ return changedIds;
18779
+ }
18780
+
18781
+ // src/init/initial-passes.ts
18782
+ async function runInitialPasses(deps) {
18783
+ const { state, sector, scheduler, skipAutoSchedule, now } = deps;
18784
+ recomputeCorrelativeIds(state);
18785
+ if (sector.updateDurationForPrimaveraEndDate) {
18786
+ recomputeDurationsFromDates(state);
18787
+ }
18788
+ normalizeLoadedConstraintDates(state);
18789
+ recomputeCanonicalRealWork(state);
18790
+ if (skipAutoSchedule) {
18791
+ if (now) {
18792
+ applyExpectedProgressLive(state, now);
18793
+ applyStatusPass(state, sector.statusCriteria);
18794
+ }
18795
+ return;
18796
+ }
18797
+ const result = await scheduler.schedule({});
18798
+ applyResults(state, result);
18799
+ normalizeMilestoneConstraintDatesForDisplay(state);
18800
+ recomputeSkippedLeafEnds(state);
18801
+ await updateParentBoundsFromChildren(state, void 0, false);
18802
+ if (now) {
18803
+ applyExpectedProgressLive(state, now);
18804
+ applyStatusPass(state, sector.statusCriteria);
18805
+ }
18806
+ }
18807
+ function recomputeSkippedLeafEnds(state) {
18808
+ state.forEachActivity((activity) => {
18809
+ if (activity.autoScheduling) return;
18810
+ if (!activity.startDate || activity.durationHours == null) return;
18811
+ if (state.getChildren(activity.id).length > 0) return;
18812
+ activity.endDate = state.calculateEndDate({
18813
+ startDate: activity.startDate,
18814
+ durationHours: activity.durationHours,
18815
+ task: activity
18816
+ });
18817
+ });
18818
+ }
18819
+ function recomputeDurationsFromDates(state) {
18820
+ state.forEachActivity((activity) => {
18821
+ if (!activity.startDate || !activity.endDate) return;
18822
+ activity.durationHours = state.calculateDuration({
18823
+ startDate: activity.startDate,
18824
+ endDate: activity.endDate,
18825
+ task: activity
18826
+ });
18827
+ });
18828
+ }
18829
+ function normalizeLoadedConstraintDates(state) {
18830
+ state.forEachActivity((activity) => {
18831
+ const type = activity.constraintType;
18832
+ if (!type || !activity.constraintDate) return;
18833
+ const constraintDate = activity.constraintDate;
18834
+ if (activity.type === "milestone" && (constraintDate.getUTCHours() !== 0 || constraintDate.getUTCMinutes() !== 0)) {
18835
+ return;
18836
+ }
18837
+ const calendar = state.calendarReader.getCalendar(
18838
+ String(activity.calendarId)
18839
+ );
18840
+ if (!calendar) return;
18841
+ activity.constraintDate = normalizeConstraintDate(
18842
+ activity.constraintDate,
18843
+ calendar,
18844
+ type
18845
+ );
18846
+ });
18847
+ }
18848
+ function normalizeMilestoneConstraintDatesForDisplay(state) {
18849
+ state.forEachActivity((activity) => {
18850
+ if (activity.type !== "milestone") return;
18851
+ const type = activity.constraintType;
18852
+ if (!type || !activity.constraintDate) return;
18853
+ const calendar = state.calendarReader.getCalendar(
18854
+ String(activity.calendarId)
18855
+ );
18856
+ if (!calendar) return;
18857
+ activity.constraintDate = normalizeConstraintDate(
18858
+ activity.constraintDate,
18859
+ calendar,
18860
+ type
18861
+ );
18862
+ });
18863
+ }
18864
+
18342
18865
  // src/init/initialize-core.ts
18343
18866
  var SCHEDULE_CORE_STATUS = {
18344
18867
  READY: "ready",
@@ -18426,48 +18949,8 @@ function initializeCore(input) {
18426
18949
  }
18427
18950
 
18428
18951
  // src/init/schedule-core.ts
18429
- function assertValidPersistenceAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
18430
- const seenIds = /* @__PURE__ */ new Set();
18431
- const seenBackendIds = /* @__PURE__ */ new Set();
18432
- for (const assignment of assignments) {
18433
- const id = String(assignment.id);
18434
- if (id.length === 0 || seenIds.has(id)) {
18435
- throw new Error(
18436
- `acknowledgePersistence: duplicate or empty ${entityName} id "${id}"`
18437
- );
18438
- }
18439
- if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0 || seenBackendIds.has(assignment.proplannerId)) {
18440
- throw new Error(
18441
- `acknowledgePersistence: invalid or duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
18442
- );
18443
- }
18444
- const state = readCurrent(id);
18445
- if (!state.exists) {
18446
- throw new Error(
18447
- `acknowledgePersistence: unknown ${entityName} id "${id}"`
18448
- );
18449
- }
18450
- if (state.current !== null && state.current !== assignment.proplannerId) {
18451
- throw new Error(
18452
- `acknowledgePersistence: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
18453
- );
18454
- }
18455
- const existingOwner = ownerByBackendId.get(assignment.proplannerId);
18456
- if (existingOwner !== void 0 && existingOwner !== id) {
18457
- throw new Error(
18458
- `acknowledgePersistence: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
18459
- );
18460
- }
18461
- seenIds.add(id);
18462
- seenBackendIds.add(assignment.proplannerId);
18463
- }
18464
- }
18465
18952
  var ScheduleCore = class {
18466
18953
  _status = SCHEDULE_CORE_STATUS.READY;
18467
- /** True while an async dispatch is suspended inside the write-capture window.
18468
- * A sync facade mutation running in that gap would corrupt the in-flight
18469
- * journal, so sync mutators refuse to run while it is set. */
18470
- _dispatchInFlight = false;
18471
18954
  coreRuntime;
18472
18955
  _undo = new UndoRecorder();
18473
18956
  _opQueue = Promise.resolve();
@@ -18518,7 +19001,7 @@ var ScheduleCore = class {
18518
19001
  return this._status;
18519
19002
  }
18520
19003
  getSector() {
18521
- return this.coreRuntime.sector;
19004
+ return cloneDomainValue(this.coreRuntime.sector);
18522
19005
  }
18523
19006
  getActivityView(id) {
18524
19007
  this.assertReady();
@@ -18544,7 +19027,7 @@ var ScheduleCore = class {
18544
19027
  this.assertReady();
18545
19028
  const snapshot = this.coreRuntime.state.getActivity(activityId);
18546
19029
  if (!snapshot) return null;
18547
- return snapshot[property];
19030
+ return cloneDomainValue(snapshot[property]);
18548
19031
  }
18549
19032
  getAllIds() {
18550
19033
  this.assertReady();
@@ -18571,7 +19054,7 @@ var ScheduleCore = class {
18571
19054
  */
18572
19055
  getVisualOrderIds() {
18573
19056
  this.assertReady();
18574
- return this.coreRuntime.state.getVisualOrderIds();
19057
+ return [...this.coreRuntime.state.getVisualOrderIds()];
18575
19058
  }
18576
19059
  hasChild(parentId) {
18577
19060
  this.assertReady();
@@ -18579,15 +19062,21 @@ var ScheduleCore = class {
18579
19062
  }
18580
19063
  getCalendar(id) {
18581
19064
  this.assertReady();
18582
- return this.coreRuntime.calendars.get(id) ?? null;
19065
+ const calendar = this.coreRuntime.calendars.get(id);
19066
+ return calendar ? cloneDomainValue(calendar) : null;
18583
19067
  }
18584
19068
  getAllCalendars() {
18585
19069
  this.assertReady();
18586
- return Array.from(this.coreRuntime.calendars.values());
19070
+ return Array.from(
19071
+ this.coreRuntime.calendars.values(),
19072
+ (calendar) => cloneDomainValue(calendar)
19073
+ );
18587
19074
  }
18588
19075
  getBaseCalendars() {
18589
19076
  this.assertReady();
18590
- return this.coreRuntime.baseCalendars;
19077
+ return this.coreRuntime.baseCalendars.map(
19078
+ (calendar) => cloneDomainValue(calendar)
19079
+ );
18591
19080
  }
18592
19081
  isCustomIdInUse(customId, currentCustomId = null) {
18593
19082
  this.assertReady();
@@ -18604,184 +19093,9 @@ var ScheduleCore = class {
18604
19093
  this.assertReady();
18605
19094
  return this._saveTracker.modifiedLinks(this.getAllLinksView());
18606
19095
  }
18607
- markLinksPersisted() {
18608
- this._saveTracker.snapshotLinks(this.getAllLinksView());
18609
- }
18610
19096
  getModifiedActivities() {
18611
19097
  this.assertReady();
18612
- return this._saveTracker.modifiedActivities(
18613
- this.coreRuntime.state.getAllActivities()
18614
- );
18615
- }
18616
- markActivitiesPersisted() {
18617
- this._saveTracker.snapshotActivities(
18618
- this.coreRuntime.state.getAllActivities()
18619
- );
18620
- }
18621
- /**
18622
- * Reconciles the backend identities returned by a successful schedule save.
18623
- *
18624
- * This is an application-boundary acknowledgement, not an editable schedule
18625
- * gesture: it does not autoschedule, recompute, emit an undo entry, or invent
18626
- * domain changes. Every assignment is validated before the first mutation so
18627
- * a malformed/partial response cannot leave the core half-reconciled. Once
18628
- * applied, the complete live graph becomes the new save-tracker baseline.
18629
- */
18630
- acknowledgePersistence(acknowledgement) {
18631
- this.assertReady();
18632
- const activities = acknowledgement.activities ?? [];
18633
- const links = acknowledgement.links ?? [];
18634
- const activityOwnerByBackendId = /* @__PURE__ */ new Map();
18635
- for (const activity of this.coreRuntime.state.getAllActivities()) {
18636
- if (activity.proplannerId != null) {
18637
- activityOwnerByBackendId.set(
18638
- activity.proplannerId,
18639
- String(activity.id)
18640
- );
18641
- }
18642
- }
18643
- const linkOwnerByBackendId = /* @__PURE__ */ new Map();
18644
- for (const link of this.coreRuntime.state.getAllLinks()) {
18645
- if (link.proplannerId != null) {
18646
- linkOwnerByBackendId.set(link.proplannerId, String(link.id));
18647
- }
18648
- }
18649
- assertValidPersistenceAssignments(
18650
- activities,
18651
- "activity",
18652
- (id) => {
18653
- const activity = this.coreRuntime.state.getActivity(id);
18654
- return {
18655
- exists: activity !== null,
18656
- current: activity?.proplannerId ?? null
18657
- };
18658
- },
18659
- activityOwnerByBackendId
18660
- );
18661
- assertValidPersistenceAssignments(
18662
- links,
18663
- "link",
18664
- (id) => {
18665
- const link = this.coreRuntime.state.getLink(id);
18666
- return {
18667
- exists: link !== null,
18668
- current: link?.proplannerId ?? null
18669
- };
18670
- },
18671
- linkOwnerByBackendId
18672
- );
18673
- this.coreRuntime.state.batchUpdate(() => {
18674
- for (const assignment of activities) {
18675
- this.coreRuntime.state.setActivityField(
18676
- assignment.id,
18677
- "proplannerId",
18678
- assignment.proplannerId
18679
- );
18680
- }
18681
- for (const assignment of links) {
18682
- this.coreRuntime.state.setLinkProplannerId(
18683
- assignment.id,
18684
- assignment.proplannerId
18685
- );
18686
- }
18687
- });
18688
- this.markActivitiesPersisted();
18689
- this.markLinksPersisted();
18690
- }
18691
- applyBaselines(points) {
18692
- this.assertReady();
18693
- const now = this.coreRuntime.clock ? endOfLocalDay(this.coreRuntime.clock()) : null;
18694
- const defaultBaseCalendarId = this.coreRuntime.baseCalendars.find((calendar) => calendar.baseDefault)?.id ?? null;
18695
- return applyBaselines(
18696
- this.coreRuntime.state,
18697
- this.coreRuntime.sector.hoursPerDay,
18698
- points,
18699
- now,
18700
- this.coreRuntime.reporter,
18701
- defaultBaseCalendarId
18702
- );
18703
- }
18704
- /**
18705
- * Whole-tree ponderator recompute (zeroes, then redistributes 100 within
18706
- * each parent from its children's active baselines, weighted by `criterion`).
18707
- * Returns the activities whose `ponderator` changed, for a prop-only repaint.
18708
- * The base rollup (`applyBaselines`) depends on fresh ponderators, so callers
18709
- * run this before it on a criterion change or baseline save.
18710
- */
18711
- recomputeAllPonderators(criterion) {
18712
- this.assertReady();
18713
- return recomputeAllPonderators2(
18714
- this.coreRuntime.state,
18715
- this.coreRuntime.sector.hoursPerDay,
18716
- criterion
18717
- );
18718
- }
18719
- /**
18720
- * Recompute the weighted-progress rollup of every parent, bottom-up.
18721
- * Returns the activities whose `progress` changed, for a prop-only repaint.
18722
- */
18723
- recomputeAllProgressRollup() {
18724
- this.assertReady();
18725
- return recomputeAllProgressRollup2(
18726
- this.coreRuntime.state,
18727
- this.coreRuntime.sector.hoursPerDay
18728
- );
18729
- }
18730
- /**
18731
- * Set the project's status criterion (the Baseline↔Actual toggle) and
18732
- * recompute every activity's `status` against it. Stores the criterion so
18733
- * future dispatches keep deriving status with it. Returns the activities
18734
- * whose `status` changed, for a prop-only repaint.
18735
- */
18736
- setStatusCriteria(criteria) {
18737
- this.assertReady();
18738
- const resolved = resolveStatusCriteria(criteria);
18739
- this.coreRuntime.sector.statusCriteria = resolved;
18740
- return recomputeStatus(
18741
- this.coreRuntime.state,
18742
- this.coreRuntime.sector.hoursPerDay,
18743
- resolved
18744
- );
18745
- }
18746
- allocateActivityId() {
18747
- this.assertReady();
18748
- return this.coreRuntime.activityIdGenerator.next();
18749
- }
18750
- createActivity(input) {
18751
- this.assertReady();
18752
- if (this._dispatchInFlight) {
18753
- throw new Error(
18754
- "[ScheduleCore] createActivity called during an in-flight dispatch"
18755
- );
18756
- }
18757
- const action = { kind: "activity-create", ...input };
18758
- this.coreRuntime.state.beginWriteCapture();
18759
- this.coreRuntime.customIdTracker.beginCustomIdTransaction();
18760
- let result;
18761
- try {
18762
- result = createActivitySync(action, {
18763
- adapter: this.coreRuntime.state,
18764
- scheduler: this.coreRuntime.scheduler,
18765
- sector: this.coreRuntime.sector,
18766
- calendars: this.coreRuntime.calendars,
18767
- activityIdGen: this.coreRuntime.activityIdGenerator,
18768
- uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
18769
- customIdTracker: this.coreRuntime.customIdTracker
18770
- });
18771
- if (!result.ok) this._rollback();
18772
- } catch (error) {
18773
- this._rollback();
18774
- throw error;
18775
- } finally {
18776
- this.coreRuntime.state.endWriteCapture();
18777
- this.coreRuntime.customIdTracker.commitCustomIdTransaction();
18778
- }
18779
- if (!result.ok) return result;
18780
- return {
18781
- ok: true,
18782
- activity: result.activity,
18783
- changes: result.changes
18784
- };
19098
+ return this._saveTracker.modifiedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
18785
19099
  }
18786
19100
  _enqueue(work) {
18787
19101
  const workPromise = this._opQueue.then(work, work);
@@ -18801,10 +19115,9 @@ var ScheduleCore = class {
18801
19115
  dateFormat: this.coreRuntime.sector.dateFormat,
18802
19116
  inputUnit: options.inputUnit ?? "days"
18803
19117
  });
18804
- const undoable = isUndoableAction(action);
19118
+ const historyPolicy = getDispatchHistoryPolicy(action);
18805
19119
  this.coreRuntime.state.beginWriteCapture();
18806
19120
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
18807
- this._dispatchInFlight = true;
18808
19121
  let result;
18809
19122
  try {
18810
19123
  result = await dispatch(action, options, {
@@ -18817,6 +19130,7 @@ var ScheduleCore = class {
18817
19130
  linkIdGen: this.coreRuntime.linkIdGenerator,
18818
19131
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
18819
19132
  customIdTracker: this.coreRuntime.customIdTracker,
19133
+ reporter: this.coreRuntime.reporter,
18820
19134
  defaultBaseCalendarId: this.coreRuntime.baseCalendars.find(
18821
19135
  (calendar) => calendar.baseDefault
18822
19136
  )?.id ?? null,
@@ -18827,11 +19141,10 @@ var ScheduleCore = class {
18827
19141
  this._rollback();
18828
19142
  throw error;
18829
19143
  } finally {
18830
- this._dispatchInFlight = false;
18831
19144
  this.coreRuntime.state.endWriteCapture();
18832
19145
  this.coreRuntime.customIdTracker.commitCustomIdTransaction();
18833
19146
  }
18834
- if (result.ok && undoable && changeSetIsSubstantive(result.changes)) {
19147
+ if (result.ok && historyPolicy === "record" && changeSetIsSubstantive(result.changes)) {
18835
19148
  const created = captureCreatedSnapshots(
18836
19149
  this.coreRuntime.state,
18837
19150
  result.changes
@@ -18848,11 +19161,18 @@ var ScheduleCore = class {
18848
19161
  Date.now()
18849
19162
  );
18850
19163
  }
19164
+ if (result.ok && historyPolicy === "clear-on-success") {
19165
+ this._saveTracker.snapshotActivities(
19166
+ this.coreRuntime.state.getAllActivities()
19167
+ );
19168
+ this._saveTracker.snapshotLinks(this.getAllLinksView());
19169
+ this._undo.clear();
19170
+ }
18851
19171
  if (!result.ok) return result;
18852
- if (changeSetIsSubstantive(result.changes)) {
19172
+ if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
18853
19173
  this._recordScheduleMutation(options.skipCriticalPath !== true);
18854
19174
  }
18855
- return { ...result, changes: result.changes };
19175
+ return cloneDomainValue(result);
18856
19176
  }
18857
19177
  /**
18858
19178
  * Starts or joins the Critical Path calculation for the current schedule
@@ -18862,9 +19182,11 @@ var ScheduleCore = class {
18862
19182
  * ineligible to commit.
18863
19183
  */
18864
19184
  recomputeCriticalPath() {
18865
- return this._enqueue(async () => ({
19185
+ const operation = this._enqueue(async () => ({
18866
19186
  job: this._startCriticalPathForCurrentRevision()
18867
19187
  })).then(({ job }) => job);
19188
+ this._criticalPathReady = operation;
19189
+ return operation;
18868
19190
  }
18869
19191
  isCriticalPathSettled() {
18870
19192
  return this._criticalPathRevision === this._scheduleRevision && this._activeCriticalPath === null;
@@ -18985,12 +19307,42 @@ var ScheduleCore = class {
18985
19307
  }
18986
19308
  return { changes };
18987
19309
  }
19310
+ /**
19311
+ * Undo/Redo restores the user's historical mutation while retaining current
19312
+ * non-historical truth (for example a refreshed baseline). Re-derive every
19313
+ * value that depends on both so the restored model is immediately coherent.
19314
+ */
19315
+ _recomputeAfterHistoryRestore() {
19316
+ const state = this.coreRuntime.state;
19317
+ recomputeAllProgressRollup(state);
19318
+ recomputeCanonicalRealWork(state);
19319
+ emitRealCost(state);
19320
+ if (this.coreRuntime.clock) {
19321
+ const now = endOfLocalDay(this.coreRuntime.clock());
19322
+ applyExpectedProgressLive(state, now);
19323
+ let hasActiveBaseline2 = false;
19324
+ state.forEachActivity((activity) => {
19325
+ hasActiveBaseline2 ||= getActiveBaseline(activity) !== null;
19326
+ });
19327
+ if (hasActiveBaseline2) {
19328
+ runExpectedProgressBase(
19329
+ state,
19330
+ now,
19331
+ this.coreRuntime.baseCalendars.find(
19332
+ (calendar) => calendar.baseDefault
19333
+ )?.id ?? null
19334
+ );
19335
+ }
19336
+ applyStatusPass(state, this.coreRuntime.sector.statusCriteria);
19337
+ }
19338
+ }
18988
19339
  undo() {
18989
19340
  const operation = this._enqueue(async () => {
18990
19341
  this.assertReady();
18991
19342
  const entry = this._undo.takeUndo();
18992
19343
  if (!entry) return null;
18993
19344
  applyUndo(this.coreRuntime.state, entry);
19345
+ this._recomputeAfterHistoryRestore();
18994
19346
  this._recordScheduleMutation(false);
18995
19347
  if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
18996
19348
  this._undo.pushRedo(entry);
@@ -19018,6 +19370,7 @@ var ScheduleCore = class {
19018
19370
  const entry = this._undo.takeRedo();
19019
19371
  if (!entry) return null;
19020
19372
  applyRedo(this.coreRuntime.state, entry);
19373
+ this._recomputeAfterHistoryRestore();
19021
19374
  this._recordScheduleMutation(false);
19022
19375
  if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
19023
19376
  this._undo.pushUndo(entry);
@@ -19039,15 +19392,20 @@ var ScheduleCore = class {
19039
19392
  );
19040
19393
  return operation;
19041
19394
  }
19042
- clearHistory() {
19043
- this._undo.clear();
19044
- }
19045
19395
  canUndo() {
19046
19396
  return this._undo.canUndo();
19047
19397
  }
19048
19398
  canRedo() {
19049
19399
  return this._undo.canRedo();
19050
19400
  }
19401
+ /**
19402
+ * Establishes a new persistence boundary without mutating schedule state.
19403
+ * Completed saves call this synchronously so neither prior undo entries nor
19404
+ * their redo branch can cross the persisted boundary.
19405
+ */
19406
+ clearHistory() {
19407
+ this._undo.clear();
19408
+ }
19051
19409
  undoDepth() {
19052
19410
  return this._undo.undoDepth();
19053
19411
  }
@@ -19133,6 +19491,10 @@ var DEFAULT_HOURS_PER_DAY = 8;
19133
19491
 
19134
19492
  // src/dispatch/action-kinds.ts
19135
19493
  var DISPATCH_ACTION_KIND = {
19494
+ PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
19495
+ BASELINE_APPLY: "baseline-apply",
19496
+ PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
19497
+ STATUS_CRITERIA_SET: "status-criteria-set",
19136
19498
  INLINE_EDIT: "inline-edit",
19137
19499
  DATES_BATCH: "dates-batch",
19138
19500
  BULK_EDIT: "bulk-edit",
@@ -19154,6 +19516,10 @@ var DISPATCH_ACTION_KIND = {
19154
19516
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
19155
19517
  };
19156
19518
  var KIND_CATALOG_COVERS_UNION = {
19519
+ [DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE]: true,
19520
+ [DISPATCH_ACTION_KIND.BASELINE_APPLY]: true,
19521
+ [DISPATCH_ACTION_KIND.PONDERATOR_CRITERION_SET]: true,
19522
+ [DISPATCH_ACTION_KIND.STATUS_CRITERIA_SET]: true,
19157
19523
  [DISPATCH_ACTION_KIND.INLINE_EDIT]: true,
19158
19524
  [DISPATCH_ACTION_KIND.DATES_BATCH]: true,
19159
19525
  [DISPATCH_ACTION_KIND.BULK_EDIT]: true,
@@ -19202,7 +19568,6 @@ exports.REJECTION_REASON = REJECTION_REASON;
19202
19568
  exports.ROOT_PARENT_ID = ROOT_PARENT_ID;
19203
19569
  exports.ScheduleCore = ScheduleCore;
19204
19570
  exports.WORK_TIME_DIRECTION = WORK_TIME_DIRECTION;
19205
- exports.applyBaselinePoints = applyBaselinePoints;
19206
19571
  exports.checkNoUpdatedLinks = checkNoUpdatedLinks;
19207
19572
  exports.computeExpectedProgress = computeExpectedProgress;
19208
19573
  exports.expectedProgressFromBaseline = expectedProgressFromBaseline;
@@ -19211,8 +19576,6 @@ exports.getUnsavedActivities = getUnsavedActivities;
19211
19576
  exports.isRootParent = isRootParent;
19212
19577
  exports.normalizeParentKey = normalizeParentKey;
19213
19578
  exports.parseFromBackend = parseFromBackend;
19214
- exports.recomputeAllPonderators = recomputeAllPonderators;
19215
- exports.recomputeAllProgressRollup = recomputeAllProgressRollup;
19216
19579
  exports.willRunCriticalPath = willRunCriticalPath;
19217
19580
  exports.yieldToBrowser = yieldToBrowser;
19218
19581
  //# sourceMappingURL=index.cjs.map