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