@outbuild-company/schedule-core 1.1.2 → 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
@@ -924,7 +924,8 @@ function expandParentLinks(links, adapter) {
924
924
  targetIds,
925
925
  link,
926
926
  adapter,
927
- virtualIdCounter
927
+ virtualIdCounter,
928
+ linksByActivity
928
929
  );
929
930
  }
930
931
  return expanded;
@@ -941,19 +942,96 @@ function expandedEndpoints(activityId, summary, adapter, linksByActivity, source
941
942
  sourceSide
942
943
  );
943
944
  }
944
- function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, adapter, virtualIdCounter) {
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
975
+ });
976
+ }
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) {
945
1003
  const source = adapter.getActivity(originalLink.source);
946
1004
  const target = adapter.getActivity(originalLink.target);
947
- const targetStart = target?.startDate;
948
- const userLag = originalLink.lag ?? 0;
1005
+ const sourceBounds = deriveSummaryBounds(originalLink.source, adapter);
1006
+ const targetBounds = deriveSummaryBounds(originalLink.target, adapter);
949
1007
  const sourceEndpointIsStart = originalLink.type === LINK_TYPE.START_TO_START || originalLink.type === LINK_TYPE.START_TO_FINISH;
950
1008
  const targetEndpointIsFinish = originalLink.type === LINK_TYPE.FINISH_TO_FINISH || originalLink.type === LINK_TYPE.START_TO_FINISH;
951
- const sourceLag = sourceEndpointIsStart ? -(source?.durationHours ?? 0) : 0;
952
- const targetLag = targetEndpointIsFinish ? -(target?.durationHours ?? 0) : 0;
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;
953
1023
  let counter = virtualIdCounter;
954
1024
  for (const srcId of sourceIds) {
1025
+ const sourceLag = sourceEndpointIsStart ? sourceLagForLeaf(srcId, source, sourceBounds, adapter) : 0;
955
1026
  for (const tgtId of leafIds) {
956
1027
  if (String(srcId) === String(tgtId)) continue;
1028
+ const offTarget = targetOffsetForLeaf(
1029
+ tgtId,
1030
+ targetStart,
1031
+ source,
1032
+ adapter,
1033
+ linksByActivity
1034
+ );
957
1035
  expanded.push(
958
1036
  buildVirtualLink(
959
1037
  originalLink,
@@ -962,10 +1040,9 @@ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, a
962
1040
  counter,
963
1041
  sourceLag,
964
1042
  targetLag,
965
- userLag,
966
- source,
967
- targetStart,
968
- adapter
1043
+ userLag + offTarget,
1044
+ summarySourceId,
1045
+ offTarget !== 0 && targetLag === 0 ? originalLink.target : void 0
969
1046
  )
970
1047
  );
971
1048
  counter += 1;
@@ -973,13 +1050,7 @@ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, a
973
1050
  }
974
1051
  return counter;
975
1052
  }
976
- function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag, targetLag, userLag, source, targetStart, adapter) {
977
- const leaf = adapter.getActivity(targetId);
978
- const offTarget = targetStart && leaf && source ? adapter.calculateDuration({
979
- startDate: targetStart,
980
- endDate: leaf.startDate,
981
- task: source
982
- }) : 0;
1053
+ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag, targetLag, trueLag, summarySourceId, offsetSummaryTargetId) {
983
1054
  return {
984
1055
  id: `virtual_${originalLink.id}_${counter}`,
985
1056
  source: sourceId,
@@ -988,7 +1059,9 @@ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag,
988
1059
  lag: originalLink.lag,
989
1060
  _sourceLag: sourceLag,
990
1061
  _targetLag: targetLag,
991
- _trueLag: userLag + offTarget
1062
+ _trueLag: trueLag,
1063
+ ...summarySourceId === void 0 ? {} : { _summarySourceId: summarySourceId },
1064
+ ...offsetSummaryTargetId === void 0 ? {} : { _summaryTargetId: offsetSummaryTargetId }
992
1065
  };
993
1066
  }
994
1067
 
@@ -1120,33 +1193,49 @@ function normalize(predecessor, successor, link) {
1120
1193
  };
1121
1194
  }
1122
1195
  }
1123
- function calculateSuccessorStartFromLink(predecessor, successor, link, adapter) {
1124
- const { sourceLag, targetLag, trueLag, isFS, isFF } = normalize(
1125
- predecessor,
1126
- 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
+ }),
1127
1216
  link
1128
- );
1129
- 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;
1130
1231
  if (!hasLag) {
1131
- const isFForFS = isFS || isFF;
1132
- if (successor.durationHours === 0 && isFForFS) {
1133
- return {
1134
- successorStart: adapter.getClosestWorkTime({
1135
- date: predecessor.endDate,
1136
- dir: "past",
1137
- task: successor
1138
- }),
1139
- link
1140
- };
1141
- }
1142
- return {
1143
- successorStart: adapter.getClosestWorkTime({
1144
- date: predecessor.endDate,
1145
- dir: "future",
1146
- task: successor
1147
- }),
1148
- link
1149
- };
1232
+ return noLagSuccessorStart(
1233
+ predecessor,
1234
+ successor,
1235
+ link,
1236
+ adapter,
1237
+ isFS || isFF
1238
+ );
1150
1239
  }
1151
1240
  const baseTask = isFS ? successor : predecessor;
1152
1241
  let date2 = adapter.getClosestWorkTime({
@@ -1367,10 +1456,12 @@ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
1367
1456
  const incoming = incomingLinks.get(String(activityId)) ?? [];
1368
1457
  let maxStart = null;
1369
1458
  let drivingLinkId = null;
1459
+ let drivingOffsetIsStale = false;
1370
1460
  for (const link of incoming) {
1371
1461
  const predecessorSnap = adapter.getActivity(link.source);
1372
1462
  if (!predecessorSnap) continue;
1373
1463
  const predDates = getActivityDates(link.source);
1464
+ const predecessorMovedThisRun = predDates.startDate.getTime() !== predecessorSnap.startDate.getTime() || predDates.endDate.getTime() !== predecessorSnap.endDate.getTime();
1374
1465
  const effectivePred = {
1375
1466
  ...predecessorSnap,
1376
1467
  startDate: predDates.startDate,
@@ -1400,8 +1491,13 @@ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
1400
1491
  if (!maxStart || result.successorStart > maxStart) {
1401
1492
  maxStart = result.successorStart;
1402
1493
  drivingLinkId = String(link.id);
1494
+ drivingOffsetIsStale = link._summaryTargetId !== void 0 && !predecessorMovedThisRun;
1403
1495
  }
1404
1496
  }
1497
+ const staleOffsetWouldPullBack = maxStart !== null && drivingOffsetIsStale && maxStart < activity.startDate;
1498
+ if (staleOffsetWouldPullBack) {
1499
+ maxStart = new Date(activity.startDate);
1500
+ }
1405
1501
  const existing = plans.get(activityId);
1406
1502
  const plan = existing ? { ...existing } : {
1407
1503
  activityId,
@@ -1569,31 +1665,33 @@ async function alapPass(reversedIds, links, asapPlans, adapter, _options, isCurr
1569
1665
  latestEnd = boundary;
1570
1666
  }
1571
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();
1572
1685
  let snappedEnd;
1573
1686
  if (activity.durationHours === 0 && successorAnchor === null) {
1574
- snappedEnd = adapter.getClosestWorkTime({
1687
+ snappedEnd = isFinishMarker && latestEndLandsInsideWorkingDay ? finishMarkerEnd() : adapter.getClosestWorkTime({
1575
1688
  date: latestEnd,
1576
1689
  dir: "past",
1577
1690
  task: activity
1578
1691
  });
1579
1692
  } else if (activity.durationHours === 0) {
1580
- const incoming = incomingLinks.get(String(activityId)) ?? [];
1581
- const isFinishKind = incoming.some(
1582
- (l) => l.type === LINK_TYPE.FINISH_TO_START || l.type === LINK_TYPE.FINISH_TO_FINISH
1583
- );
1584
- if (isFinishKind) {
1585
- const dayStart = new Date(
1586
- Date.UTC(
1587
- latestEnd.getUTCFullYear(),
1588
- latestEnd.getUTCMonth(),
1589
- latestEnd.getUTCDate()
1590
- )
1591
- );
1592
- snappedEnd = adapter.getClosestWorkTime({
1593
- date: dayStart,
1594
- dir: "past",
1595
- task: activity
1596
- });
1693
+ if (isFinishMarker) {
1694
+ snappedEnd = finishMarkerEnd();
1597
1695
  } else {
1598
1696
  snappedEnd = adapter.getClosestWorkTime({
1599
1697
  date: latestEnd,
@@ -1626,10 +1724,16 @@ async function alapPass(reversedIds, links, asapPlans, adapter, _options, isCurr
1626
1724
  plan.endDate = snappedEnd;
1627
1725
  plan.drivingLinkId = null;
1628
1726
  plan.kind = "alap";
1727
+ const predecessorFloor = plan.earliestSchedulingStart ?? null;
1629
1728
  plan.earliestSchedulingStart = null;
1630
1729
  plan.earliestSchedulingEnd = null;
1631
- plan.latestSchedulingStart = snappedStart;
1632
- 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
+ });
1633
1737
  limitPlanDates(activity, plan, adapter);
1634
1738
  plans.set(activityId, plan);
1635
1739
  }
@@ -2064,18 +2168,9 @@ var AutoScheduler = class {
2064
2168
  }
2065
2169
  const phaseScope = startPhase("schedule.scoping");
2066
2170
  const allIds = this.adapter.getAllIds();
2067
- let scopedLinks = activeLinks;
2068
- let scopedIds = allIds;
2069
- let scopedIdSet = null;
2070
- if (options.triggerId !== void 0 && options.triggerId !== null) {
2071
- const group = findConnectedGroupForTrigger(
2072
- options.triggerId,
2073
- activeLinks
2074
- );
2075
- scopedLinks = group.links;
2076
- scopedIds = Array.from(group.activityIds);
2077
- scopedIdSet = new Set(scopedIds);
2078
- }
2171
+ const scope = resolveScope(options, activeLinks, this.adapter);
2172
+ const scopedLinks = scope.links;
2173
+ const scopedIdSet = scope.idSet;
2079
2174
  endPhase(phaseScope);
2080
2175
  const phaseTopo = startPhase("schedule.topologicalSort");
2081
2176
  const topoResult = resolveTopoResult(
@@ -2172,6 +2267,34 @@ function endPhase(handle) {
2172
2267
  const elapsed = performance.now() - handle.startedAt;
2173
2268
  console.log(`[scheduler] ${handle.name} ${elapsed.toFixed(1)}ms`);
2174
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
+ }
2175
2298
  function resolveTopoResult(cache, allIds, allLinks, scopedIdSet) {
2176
2299
  const fullTopo = cache.get(allIds, allLinks);
2177
2300
  if (fullTopo.hasCycles) {
@@ -2189,9 +2312,10 @@ function resolveTopoResult(cache, allIds, allLinks, scopedIdSet) {
2189
2312
  }
2190
2313
 
2191
2314
  // src/internal/post-processors/adjust-link-lag-on-task-move.ts
2192
- function adjustLinkLagOnTaskMove(targetId, adapter) {
2315
+ function adjustLinkLagOnTaskMove(targetId, adapter, preEditTarget) {
2193
2316
  const target = adapter.getActivity(targetId);
2194
2317
  if (!target) return;
2318
+ const targetForLagMath = preEditTarget ? { ...target, endDate: preEditTarget.endDate } : target;
2195
2319
  const calendarApi = adapter.calendarReader.getCalendar(
2196
2320
  String(target.calendarId)
2197
2321
  );
@@ -2201,9 +2325,11 @@ function adjustLinkLagOnTaskMove(targetId, adapter) {
2201
2325
  const source = adapter.getActivity(link.source);
2202
2326
  if (!source) continue;
2203
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;
2204
2330
  const { sourceDate, targetDate } = pickDatesForLinkType(
2205
2331
  source,
2206
- target,
2332
+ targetForLagMath,
2207
2333
  link.type
2208
2334
  );
2209
2335
  const newLag = dateMath.computeLagBetweenTasks(
@@ -2251,14 +2377,15 @@ function computeLeafRealWork(workHours, progress) {
2251
2377
  // src/propagations/upward/parent-bounds.ts
2252
2378
  async function updateParentBoundsFromChildren(adapter, dirtyIds, recomputeProgress = true, options = {}) {
2253
2379
  const parents = dirtyIds ? findAncestorsOfDirty(adapter, dirtyIds) : findAllParents(adapter);
2254
- await recomputeParentsCooperatively(parents, adapter, recomputeProgress, {
2380
+ const progressMode = recomputeProgress ? options.progressMode ?? "freeze-when-no-contribution" : "skip";
2381
+ await recomputeParentsCooperatively(parents, adapter, progressMode, {
2255
2382
  recomputeRealWork: options.recomputeRealWork ?? false
2256
2383
  });
2257
2384
  }
2258
2385
  function recomputeSingleParentDisplayBounds(parentId, adapter) {
2259
- recomputeParentFromChildren(parentId, adapter, false);
2386
+ recomputeParentFromChildren(parentId, adapter, "skip");
2260
2387
  }
2261
- async function recomputeParentsCooperatively(parents, adapter, recomputeProgress = true, options = {
2388
+ async function recomputeParentsCooperatively(parents, adapter, progressMode, options = {
2262
2389
  recomputeRealWork: false
2263
2390
  }) {
2264
2391
  const YIELD_EVERY_N_PARENTS = 200;
@@ -2267,7 +2394,7 @@ async function recomputeParentsCooperatively(parents, adapter, recomputeProgress
2267
2394
  recomputeParentFromChildren(
2268
2395
  parentId,
2269
2396
  adapter,
2270
- recomputeProgress,
2397
+ progressMode,
2271
2398
  options.recomputeRealWork
2272
2399
  );
2273
2400
  processed += 1;
@@ -2317,7 +2444,7 @@ function aggregateChildren(childrenIds, adapter, includeRealWork) {
2317
2444
  }
2318
2445
  return { minStart, maxEnd, realWorkSum, weightedChildren };
2319
2446
  }
2320
- function recomputeParentFromChildren(parentId, adapter, recomputeProgress = true, recomputeRealWork = false) {
2447
+ function recomputeParentFromChildren(parentId, adapter, progressMode = "freeze-when-no-contribution", recomputeRealWork = false) {
2321
2448
  const parent = adapter.getActivity(parentId);
2322
2449
  if (!parent) return;
2323
2450
  const childrenIds = adapter.getChildren(parentId);
@@ -2335,11 +2462,12 @@ function recomputeParentFromChildren(parentId, adapter, recomputeProgress = true
2335
2462
  const progressRollup = computeWeightedProgressRollup(
2336
2463
  aggregate.weightedChildren
2337
2464
  );
2338
- if (recomputeProgress && progressRollup !== null) {
2465
+ const shouldWriteProgress = progressMode !== "skip" && (progressRollup !== null || progressMode === "zero-when-no-contribution");
2466
+ if (shouldWriteProgress) {
2339
2467
  adapter.setActivityField(
2340
2468
  parentId,
2341
2469
  "progress",
2342
- roundProgressPerLevel(progressRollup)
2470
+ roundProgressPerLevel(progressRollup ?? 0)
2343
2471
  );
2344
2472
  }
2345
2473
  if (recomputeRealWork) {
@@ -2401,7 +2529,7 @@ function runRecordLastStartDate(activityId, adapter) {
2401
2529
  const activity = adapter.getActivity(activityId);
2402
2530
  if (activity) adapter.setLastStartDate(activityId, activity.startDate);
2403
2531
  }
2404
- function runPostProcessorsOnAdapter(activityId, processorNames, adapter) {
2532
+ function runPostProcessorsOnAdapter(activityId, processorNames, adapter, preEditActivity) {
2405
2533
  for (const name of processorNames) {
2406
2534
  const fn = name in POST_PROCESSORS ? POST_PROCESSORS[name] : void 0;
2407
2535
  if (!fn) {
@@ -2410,7 +2538,7 @@ function runPostProcessorsOnAdapter(activityId, processorNames, adapter) {
2410
2538
  );
2411
2539
  continue;
2412
2540
  }
2413
- fn(activityId, adapter);
2541
+ fn(activityId, adapter, preEditActivity ?? null);
2414
2542
  }
2415
2543
  }
2416
2544
  function runNoOp(name) {
@@ -2683,10 +2811,7 @@ var durationPipeline = {
2683
2811
  );
2684
2812
  return {
2685
2813
  ...fieldChanges(extraFields, {
2686
- // MAIN treats an ALAP duration edit as a resize from Start: Start is
2687
- // anchored and Finish moves. Running the ALAP pass would instead
2688
- // preserve Finish and pull Start, which is not the observed UI flow.
2689
- autoSchedule: activity.constraintType !== "alap",
2814
+ autoSchedule: true,
2690
2815
  postProcessors: [POST_PROCESSOR.RECORD_LAST_START_DATE]
2691
2816
  }),
2692
2817
  ...constraintCheck.violated ? {
@@ -2848,16 +2973,22 @@ function buildCanonicalRealWorkOverlay(activity, progress, hierarchy) {
2848
2973
  realWorkByActivity.set(activity.id, selfRealWork);
2849
2974
  return realWorkByActivity;
2850
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
+ }
2851
2982
  function buildAncestorRollupCascade(activity, newValue, hierarchy) {
2852
2983
  const overlay = /* @__PURE__ */ new Map();
2853
2984
  overlay.set(activity.id, newValue);
2854
2985
  for (const descendantId of hierarchy.getDescendantIds(activity.id)) {
2855
2986
  overlay.set(descendantId, newValue);
2856
2987
  }
2857
- const chain = [];
2858
- for (const ancestorId of hierarchy.getAncestorIds(activity.id)) {
2859
- chain.push(ancestorId);
2860
- }
2988
+ const chain = [
2989
+ ...summariesOverSummariesDeepestFirst(activity, hierarchy),
2990
+ ...hierarchy.getAncestorIds(activity.id)
2991
+ ];
2861
2992
  const mutations = [];
2862
2993
  for (const id of chain) {
2863
2994
  const rollup = computeWeightedProgress(id, overlay, hierarchy);
@@ -3254,6 +3385,8 @@ function checkIsWorkingDay(newDate, activity, ctx) {
3254
3385
  return null;
3255
3386
  }
3256
3387
  function detectEndDateConstraintWarning(activity, newEndDate) {
3388
+ const cannotViolateStartPin = effectiveConstraintType(activity) === CONSTRAINT_TYPE2.MSO;
3389
+ if (cannotViolateStartPin) return void 0;
3257
3390
  const projectedDate = pickProjectedDateForConstraint(
3258
3391
  effectiveConstraintType(activity),
3259
3392
  activity.startDate,
@@ -3274,7 +3407,8 @@ function detectEndDateConstraintWarning(activity, newEndDate) {
3274
3407
  function buildEndDateExtraFields(activity, newEndDate, ctx, violatesConstraint = false) {
3275
3408
  const fields = {};
3276
3409
  const isMilestone = isMilestoneType(activity.type);
3277
- if (isMilestone && !violatesConstraint) {
3410
+ const seedsMilestoneDate = isMilestone && !violatesConstraint && hasDatelessConstraintType(activity);
3411
+ if (seedsMilestoneDate) {
3278
3412
  Object.assign(fields, setConstraintDate(activity.startDate));
3279
3413
  } else if (violatesConstraint || hasDatelessConstraintType(activity)) {
3280
3414
  Object.assign(
@@ -3284,10 +3418,8 @@ function buildEndDateExtraFields(activity, newEndDate, ctx, violatesConstraint =
3284
3418
  );
3285
3419
  }
3286
3420
  const newDuration = calculateNewDuration(activity, newEndDate, ctx);
3287
- if (!isMilestone) {
3288
- const flippedType = typeForNewDuration(activity.durationHours, newDuration);
3289
- if (flippedType) fields.type = flippedType;
3290
- }
3421
+ const flippedType = typeForNewDuration(activity.durationHours, newDuration);
3422
+ if (flippedType) fields.type = flippedType;
3291
3423
  Object.assign(fields, setDuration(newDuration));
3292
3424
  fields.endDate = newEndDate;
3293
3425
  return fields;
@@ -3704,6 +3836,49 @@ function setLinkFieldDynamic(state, linkId, field, value) {
3704
3836
  state.setLinkFieldDynamic(linkId, field, value);
3705
3837
  }
3706
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
+
3707
3882
  // src/dispatch/shared/snapshots.ts
3708
3883
  function collectTouchedIds(primary, changes) {
3709
3884
  const ids = /* @__PURE__ */ new Set([String(primary)]);
@@ -3721,13 +3896,7 @@ function snapshotActivities(adapter, ids) {
3721
3896
  return out;
3722
3897
  }
3723
3898
  function structuredCloneActivity(activity) {
3724
- return {
3725
- ...activity,
3726
- startDate: new Date(activity.startDate),
3727
- endDate: new Date(activity.endDate),
3728
- constraintDate: activity.constraintDate ? new Date(activity.constraintDate) : null,
3729
- dateOrigin: activity.dateOrigin ? new Date(activity.dateOrigin) : null
3730
- };
3899
+ return cloneCoreActivity(activity);
3731
3900
  }
3732
3901
  function applyFieldChanges(adapter, activityId, changes) {
3733
3902
  applyCanonicalPatch(adapter, activityId, changes.patch);
@@ -3780,12 +3949,33 @@ function diffActivity(before, after) {
3780
3949
  const afterRec = after;
3781
3950
  const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRec), ...Object.keys(afterRec)]);
3782
3951
  for (const k of keys) {
3783
- if (!shallowEqual(beforeRec[k], afterRec[k])) {
3952
+ if (Object.hasOwn(beforeRec, k) !== Object.hasOwn(afterRec, k) || !fieldValueEqual(beforeRec[k], afterRec[k])) {
3784
3953
  fields[k] = { before: beforeRec[k], after: afterRec[k] };
3785
3954
  }
3786
3955
  }
3787
3956
  return fields;
3788
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
+ }
3789
3979
  function collectDirtyForInlineEdit(triggerId, touchedIds, scheduledIds) {
3790
3980
  const dirty = /* @__PURE__ */ new Set();
3791
3981
  dirty.add(triggerId);
@@ -3811,21 +4001,6 @@ function foldCorrelativeShifts(adapter, shifts, excludeIds, beforeSnap, touched)
3811
4001
  beforeSnap.set(shift.activityId, beforeImage);
3812
4002
  }
3813
4003
  }
3814
- function shallowEqual(a, b) {
3815
- if (a === b) return true;
3816
- if (a instanceof Date && b instanceof Date)
3817
- return a.getTime() === b.getTime();
3818
- if (a == null || b == null) return false;
3819
- if (typeof a !== typeof b) return false;
3820
- if (Array.isArray(a) && Array.isArray(b)) {
3821
- if (a.length !== b.length) return false;
3822
- for (let i = 0; i < a.length; i++) {
3823
- if (!shallowEqual(a[i], b[i])) return false;
3824
- }
3825
- return true;
3826
- }
3827
- return false;
3828
- }
3829
4004
 
3830
4005
  // src/shared/backend-date.ts
3831
4006
  var BACKEND_DATE_FORMAT_REGEX = /^(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{2})$/;
@@ -9355,6 +9530,9 @@ function resolveDerivedPasses(action, autoSchedule, options) {
9355
9530
  // src/dispatch/shared/post-mutation.ts
9356
9531
  async function runPostMutation(deps, args) {
9357
9532
  const passes = resolvePasses(args);
9533
+ if (passes.has(DERIVED_PASS.AUTOSCHEDULE)) {
9534
+ await refreshMutatedParentBounds(deps.adapter, args);
9535
+ }
9358
9536
  const schedule = passes.has(DERIVED_PASS.AUTOSCHEDULE) ? await runAutoschedule(deps, args.autoscheduleFrom) : emptySchedule();
9359
9537
  await rollUpParentBounds(deps.adapter, args, schedule.scheduledIds, passes);
9360
9538
  await runDerivedRecomputes(deps, passes, args.now);
@@ -9363,7 +9541,11 @@ async function runPostMutation(deps, args) {
9363
9541
  function resolvePasses(args) {
9364
9542
  const requested = args.autoscheduleFrom !== null;
9365
9543
  const ran = !args.options.skipAutoSchedule && requested;
9366
- 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;
9367
9549
  }
9368
9550
  async function runAutoschedule(deps, autoscheduleFrom) {
9369
9551
  const options = autoscheduleFrom === "roots" ? {} : { triggerId: autoscheduleFrom };
@@ -9374,20 +9556,23 @@ async function runAutoschedule(deps, autoscheduleFrom) {
9374
9556
  }
9375
9557
  function captureScheduledRows(adapter, updatedIds) {
9376
9558
  const scheduledIds = [];
9377
- const scheduledBeforeImages = /* @__PURE__ */ new Map();
9378
9559
  for (const id of updatedIds) {
9379
9560
  const activityId = String(id);
9380
9561
  scheduledIds.push(activityId);
9381
9562
  const snap = adapter.getActivity(activityId);
9382
9563
  if (snap) {
9383
- scheduledBeforeImages.set(activityId, structuredCloneActivity(snap));
9384
9564
  adapter.noteScheduledBefore(activityId, snap);
9385
9565
  }
9386
9566
  }
9387
- return { scheduledIds, scheduledBeforeImages };
9567
+ return { scheduledIds };
9388
9568
  }
9389
9569
  function emptySchedule() {
9390
- 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);
9391
9576
  }
9392
9577
  async function rollUpParentBounds(adapter, args, scheduledIds, passes) {
9393
9578
  const dirty = args.collectDirty ? args.collectDirty(scheduledIds) : collectDirtyForStructural(args.recomputeParentsFrom, scheduledIds);
@@ -9396,17 +9581,20 @@ async function rollUpParentBounds(adapter, args, scheduledIds, passes) {
9396
9581
  dirty,
9397
9582
  passes.has(DERIVED_PASS.PARENT_PROGRESS),
9398
9583
  {
9399
- 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"
9400
9586
  }
9401
9587
  );
9402
9588
  }
9403
9589
  async function runDerivedRecomputes(deps, passes, now) {
9404
9590
  if (passes.has(DERIVED_PASS.EXPECTED_PROGRESS) && now !== null) {
9405
- runExpectedProgressBase(
9406
- deps.adapter,
9407
- now,
9408
- deps.defaultBaseCalendarId ?? null
9409
- );
9591
+ if (hasActiveBaseline(deps.adapter)) {
9592
+ runExpectedProgressBase(
9593
+ deps.adapter,
9594
+ now,
9595
+ deps.defaultBaseCalendarId ?? null
9596
+ );
9597
+ }
9410
9598
  applyExpectedProgressLive(deps.adapter, now);
9411
9599
  applyStatusPass(deps.adapter, deps.sector.statusCriteria);
9412
9600
  }
@@ -9417,6 +9605,13 @@ async function runDerivedRecomputes(deps, passes, now) {
9417
9605
  await applyCriticalPath(deps.adapter, deps.sector.hoursPerDay);
9418
9606
  }
9419
9607
  }
9608
+ function hasActiveBaseline(adapter) {
9609
+ let active = false;
9610
+ adapter.forEachActivity((activity) => {
9611
+ if (getActiveBaseline(activity)) active = true;
9612
+ });
9613
+ return active;
9614
+ }
9420
9615
 
9421
9616
  // src/dispatch/handlers/revert-no-op-constraint-edit.ts
9422
9617
  var CONSTRAINT_EDIT_COLUMNS = /* @__PURE__ */ new Set([
@@ -9427,6 +9622,9 @@ var MUST_CONSTRAINTS = /* @__PURE__ */ new Set(["mso", "mfo"]);
9427
9622
  function isConstraintEditColumn(column) {
9428
9623
  return CONSTRAINT_EDIT_COLUMNS.has(column);
9429
9624
  }
9625
+ function isExplicitConstraintDateEdit(column) {
9626
+ return column === COLUMN.CONSTRAINT_DATE;
9627
+ }
9430
9628
  function revertNoOpConstraintEdit(activityId, priorConstraintDate, adapter, calendars) {
9431
9629
  const activity = adapter.getActivity(activityId);
9432
9630
  if (!activity) return false;
@@ -9470,7 +9668,7 @@ function sameDate(first, second) {
9470
9668
 
9471
9669
  // src/dispatch/shared/constraint-settle.ts
9472
9670
  async function settleConstraintEdits(deps, { postMutationArgs, revertTargets, revert }) {
9473
- let { scheduledIds, scheduledBeforeImages } = await runPostMutation(
9671
+ let { scheduledIds } = await runPostMutation(
9474
9672
  deps,
9475
9673
  withCriticalPathSuppressed(postMutationArgs)
9476
9674
  );
@@ -9485,15 +9683,11 @@ async function settleConstraintEdits(deps, { postMutationArgs, revertTargets, re
9485
9683
  withCriticalPathSuppressed(postMutationArgs)
9486
9684
  );
9487
9685
  scheduledIds = mergeScheduledIds(scheduledIds, resettled.scheduledIds);
9488
- scheduledBeforeImages = mergeScheduledBeforeImagesFirstWins(
9489
- scheduledBeforeImages,
9490
- resettled.scheduledBeforeImages
9491
- );
9492
9686
  }
9493
9687
  if (resolveRunCriticalPath(postMutationArgs.action)) {
9494
9688
  await runPostMutation(deps, criticalPathOnlyArgs(postMutationArgs));
9495
9689
  }
9496
- return { scheduledIds, scheduledBeforeImages };
9690
+ return { scheduledIds };
9497
9691
  }
9498
9692
  function withCriticalPathSuppressed(args) {
9499
9693
  return {
@@ -9515,13 +9709,6 @@ function mergeScheduledIds(firstSettleIds, secondSettleIds) {
9515
9709
  for (const scheduledId of secondSettleIds) merged.add(scheduledId);
9516
9710
  return [...merged];
9517
9711
  }
9518
- function mergeScheduledBeforeImagesFirstWins(firstSettleImages, secondSettleImages) {
9519
- const merged = new Map(firstSettleImages);
9520
- for (const [imageId, image] of secondSettleImages) {
9521
- if (!merged.has(imageId)) merged.set(imageId, image);
9522
- }
9523
- return merged;
9524
- }
9525
9712
 
9526
9713
  // src/dispatch/sir-auto-reject.ts
9527
9714
  function datesChanged(before, after) {
@@ -9556,33 +9743,12 @@ function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
9556
9743
 
9557
9744
  // src/dispatch/shared/change-set.ts
9558
9745
  function assembleChangeSet(adapter, args) {
9559
- const allTouched = /* @__PURE__ */ new Set();
9560
- for (const id of args.touchedIds) allTouched.add(id);
9561
- for (const id of args.scheduledIds) allTouched.add(id);
9562
- if (args.includeAllIds) {
9563
- for (const id of adapter.getAllIds()) allTouched.add(id);
9564
- }
9746
+ const allTouched = collectChangeSetIds(adapter, args);
9565
9747
  const before = new Map(args.beforeSnap);
9566
9748
  const captured = adapter.peekWriteCapture();
9567
9749
  if (captured) {
9568
- for (const id of captured.dirty) {
9569
- if (allTouched.has(id)) continue;
9570
- const beforeImg = captured.before.get(id);
9571
- const live = adapter.getActivity(id);
9572
- if (!beforeImg || !live) continue;
9573
- if (Object.keys(diffActivity(beforeImg, live)).length === 0) continue;
9574
- before.set(id, beforeImg);
9575
- allTouched.add(id);
9576
- }
9577
- }
9578
- if (args.scheduledBeforeImages) {
9579
- for (const [id, img] of args.scheduledBeforeImages) {
9580
- const sid = String(id);
9581
- if (!before.has(sid)) {
9582
- before.set(sid, img);
9583
- allTouched.add(sid);
9584
- }
9585
- }
9750
+ const insertedIds = collectInsertedIds(captured);
9751
+ mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
9586
9752
  }
9587
9753
  const beforeDiffEffects = args.sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
9588
9754
  const activityChanges = buildActivityChanges(adapter, before, allTouched);
@@ -9599,6 +9765,31 @@ function assembleChangeSet(adapter, args) {
9599
9765
  ...warnings.length > 0 ? { warnings } : {}
9600
9766
  };
9601
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
+ }
9602
9793
 
9603
9794
  // src/dispatch/conversion.ts
9604
9795
  function snapshotToLink(l) {
@@ -9781,7 +9972,8 @@ async function dispatchInlineEdit(action, options, deps) {
9781
9972
  runPostProcessorsOnAdapter(
9782
9973
  action.activityId,
9783
9974
  changes.postProcessors ?? [],
9784
- adapter
9975
+ adapter,
9976
+ beforeSnap.get(String(action.activityId)) ?? null
9785
9977
  );
9786
9978
  const linkChanges = diffIncomingLinkLagChanges(
9787
9979
  beforeLinkLags,
@@ -9806,7 +9998,7 @@ async function dispatchInlineEdit(action, options, deps) {
9806
9998
  options
9807
9999
  });
9808
10000
  const isConstraintPath = isConstraintEditColumn(action.column);
9809
- const { scheduledIds, scheduledBeforeImages } = await (isConstraintPath ? settleConstraintEdits(
10001
+ const { scheduledIds } = await (isConstraintPath ? settleConstraintEdits(
9810
10002
  {
9811
10003
  adapter,
9812
10004
  scheduler,
@@ -9815,9 +10007,7 @@ async function dispatchInlineEdit(action, options, deps) {
9815
10007
  },
9816
10008
  {
9817
10009
  postMutationArgs: buildPostMutationArgs(),
9818
- revertTargets: [
9819
- { activityId: action.activityId, priorConstraintDate }
9820
- ],
10010
+ revertTargets: isExplicitConstraintDateEdit(action.column) ? [] : [{ activityId: action.activityId, priorConstraintDate }],
9821
10011
  revert: (activityId, capturedConstraintDate) => revertNoOpConstraintEdit(
9822
10012
  activityId,
9823
10013
  capturedConstraintDate,
@@ -9849,7 +10039,6 @@ async function dispatchInlineEdit(action, options, deps) {
9849
10039
  beforeSnap,
9850
10040
  touchedIds,
9851
10041
  scheduledIds,
9852
- scheduledBeforeImages,
9853
10042
  sirDetection: "before-diff",
9854
10043
  links: linkChanges,
9855
10044
  trackingEvents,
@@ -9863,6 +10052,40 @@ function columnInvalidatesExpandedLinks(column) {
9863
10052
  }
9864
10053
 
9865
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
+ }
9866
10089
  async function dispatchLink(action, options, deps) {
9867
10090
  if (action.kind === "link-create" || action.kind === "link-update") {
9868
10091
  const type = action.type;
@@ -9920,40 +10143,14 @@ function batchOpToLinkOperation(op, hoursPerDay) {
9920
10143
  async function dispatchLinkBatch(operations, source, options, deps, deterministicIds) {
9921
10144
  const { adapter, scheduler, sector, linkIdGen } = deps;
9922
10145
  scheduler.invalidateAllCaches();
9923
- const affectedActivityIds = /* @__PURE__ */ new Set();
9924
- const beforeLinks = /* @__PURE__ */ new Map();
9925
- for (const op of operations) {
9926
- if (op.kind === "create") {
9927
- affectedActivityIds.add(String(op.source));
9928
- affectedActivityIds.add(String(op.target));
9929
- } else {
9930
- const link = adapter.getLink(op.linkId);
9931
- if (link) {
9932
- affectedActivityIds.add(String(link.source));
9933
- affectedActivityIds.add(String(link.target));
9934
- beforeLinks.set(op.linkId, { ...link });
9935
- }
9936
- }
9937
- }
10146
+ const { activityIds: affectedActivityIds, links: beforeLinks } = collectBatchContext(operations, adapter);
9938
10147
  const beforeActivities = snapshotActivities(adapter, affectedActivityIds);
9939
- const applied = [];
9940
- for (let i = 0; i < operations.length; i++) {
9941
- const op = operations[i];
9942
- const detId = op.kind === "create" && deterministicIds ? deterministicIds.get(i) : void 0;
9943
- const result = applyLinkOperation(op, {
9944
- port: adapter,
9945
- // Si el caller pasó un id deterministic (test override, o el bridge
9946
- // pasando el id que DHTMLX iba a usar), lo respetamos. Si no, el
9947
- // generador del core arranca desde Date.now() — mismo formato que
9948
- // DHTMLX uid(). Ver `generators/link-id-generator.ts`.
9949
- newLinkId: detId ? () => detId : linkIdGen.next
9950
- });
9951
- applied.push({
9952
- op,
9953
- finalLinkId: result.applied ? result.linkId : null,
9954
- rejected: result.applied ? null : result.rejected ?? "link_op_rejected"
9955
- });
9956
- }
10148
+ const applied = applyBatchOperations(
10149
+ operations,
10150
+ adapter,
10151
+ linkIdGen,
10152
+ deterministicIds
10153
+ );
9957
10154
  if (operations.length === 1 && applied[0] && applied[0].rejected !== null && source.kind !== "inline-edit") {
9958
10155
  return { ok: false, reason: applied[0].rejected };
9959
10156
  }
@@ -9973,20 +10170,23 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
9973
10170
  options
9974
10171
  }
9975
10172
  );
9976
- for (const id of scheduledIds) {
9977
- if (!beforeActivities.has(id)) {
9978
- const a = adapter.getActivity(id);
9979
- if (a) beforeActivities.set(id, structuredCloneActivity(a));
9980
- }
9981
- }
9982
10173
  const linkChanges = buildLinkChangesForBatch(applied, beforeLinks, adapter);
9983
- const beforeLinksByStringId = /* @__PURE__ */ new Map();
9984
- for (const [linkId, snap] of beforeLinks) {
9985
- beforeLinksByStringId.set(String(linkId), snap);
9986
- }
9987
10174
  return {
9988
10175
  ok: true,
9989
- __beforeLinks: beforeLinksByStringId,
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),
9990
10190
  changes: assembleChangeSet(adapter, {
9991
10191
  source,
9992
10192
  beforeSnap: beforeActivities,
@@ -10359,8 +10559,7 @@ var NEW_ACTIVITY_DEFAULTS = Object.freeze({
10359
10559
  expectedProgress: null,
10360
10560
  expectedProgressBaseline: null,
10361
10561
  status: null,
10362
- criticalPath: null,
10363
- promotionRestore: null
10562
+ criticalPath: null
10364
10563
  });
10365
10564
  var FIRST_ACTIVITY_TEXT = "New Master Plan";
10366
10565
  var NEW_ACTIVITY_TEXT = "New Activity";
@@ -10465,6 +10664,23 @@ function resolveStartDate(parent, fallback, override) {
10465
10664
  }
10466
10665
 
10467
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
+ }
10468
10684
  function buildParentMutations(input) {
10469
10685
  const parent = input.getParent(input.parentId);
10470
10686
  if (!parent) return null;
@@ -10483,14 +10699,18 @@ function buildParentMutations(input) {
10483
10699
  }
10484
10700
  if (wasPromotableLeaf) {
10485
10701
  fields.type = PROMOTION_TARGET_TYPE;
10486
- fields.constraintType = CONSTRAINT_TYPE2.ASAP;
10487
- fields.constraintDate = null;
10488
10702
  if ((input.promotionSource ?? "create") === "create") {
10703
+ fields.constraintType = CONSTRAINT_TYPE2.ASAP;
10704
+ fields.constraintDate = null;
10489
10705
  fields.progress = 0;
10490
10706
  fields.expectedProgressBaseline = 0;
10491
10707
  }
10492
10708
  }
10493
- return { parentId: input.parentId, fields };
10709
+ return {
10710
+ parentId: input.parentId,
10711
+ fields,
10712
+ promotionSnapshot: snapshotOf(parent)
10713
+ };
10494
10714
  }
10495
10715
  function isPromotableLeaf(parent) {
10496
10716
  return isPromotableLeafType(parent.type) && (!Array.isArray(parent.newActivityIds) || parent.newActivityIds.length === 0);
@@ -10716,6 +10936,12 @@ function applyParentMutations(action, newId, parent, adapter, preserveParentCust
10716
10936
  if (skipCustomIdMutation) continue;
10717
10937
  setActivityFieldDynamic(adapter, parentMutations.parentId, key, value);
10718
10938
  }
10939
+ if (parentMutations.promotionSnapshot) {
10940
+ adapter.setPromotionSnapshot(
10941
+ parentMutations.parentId,
10942
+ parentMutations.promotionSnapshot
10943
+ );
10944
+ }
10719
10945
  }
10720
10946
  function recomputeAndFoldCorrelatives(adapter, newId, beforeSnap, initialTouched, opts) {
10721
10947
  if (opts.skipCorrelativeRecompute) return;
@@ -10729,7 +10955,7 @@ function recomputeAndFoldCorrelatives(adapter, newId, beforeSnap, initialTouched
10729
10955
  initialTouched
10730
10956
  );
10731
10957
  }
10732
- function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPerDay, scheduledBeforeImages) {
10958
+ function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPerDay) {
10733
10959
  const { newId, newActivity, beforeSnap, initialTouched } = coreResult;
10734
10960
  const trackingEvent = {
10735
10961
  name: DISPATCH_TRACK_EVENT.ACTIVITY_CREATION,
@@ -10742,17 +10968,11 @@ function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPe
10742
10968
  if (action.eventSource !== void 0) {
10743
10969
  trackingEvent.properties.event_source = action.eventSource;
10744
10970
  }
10745
- const existingScheduledBeforeImages = scheduledBeforeImages ? new Map(
10746
- [...scheduledBeforeImages].filter(
10747
- ([scheduledId]) => String(scheduledId) !== String(newId)
10748
- )
10749
- ) : void 0;
10750
10971
  return assembleChangeSet(adapter, {
10751
10972
  source: action,
10752
10973
  beforeSnap,
10753
10974
  touchedIds: initialTouched,
10754
10975
  scheduledIds,
10755
- scheduledBeforeImages: existingScheduledBeforeImages,
10756
10976
  trackingEvents: [trackingEvent]});
10757
10977
  }
10758
10978
  async function dispatchActivityCreate(action, options, deps) {
@@ -10782,14 +11002,13 @@ async function dispatchActivityCreate(action, options, deps) {
10782
11002
  } finally {
10783
11003
  adapter.setActivityField(newId, "autoScheduling", createdAutoScheduling);
10784
11004
  }
10785
- const { scheduledIds, scheduledBeforeImages } = scheduleOutcome;
11005
+ const { scheduledIds } = scheduleOutcome;
10786
11006
  const changeset = buildCreateChangeSet(
10787
11007
  adapter,
10788
11008
  action,
10789
11009
  core,
10790
11010
  scheduledIds,
10791
- sector.hoursPerDay,
10792
- scheduledBeforeImages
11011
+ sector.hoursPerDay
10793
11012
  );
10794
11013
  return { ok: true, changes: changeset };
10795
11014
  }
@@ -10934,7 +11153,7 @@ async function dispatchActivityPaste(action, options, deps) {
10934
11153
  rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
10935
11154
  });
10936
11155
  }
10937
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11156
+ const { scheduledIds } = await runPostMutation(
10938
11157
  {
10939
11158
  adapter,
10940
11159
  scheduler,
@@ -10961,14 +11180,6 @@ async function dispatchActivityPaste(action, options, deps) {
10961
11180
  beforeSnap,
10962
11181
  touchedIds: touched,
10963
11182
  scheduledIds,
10964
- // Existing rows the autoscheduler moved need a before-image (the global
10965
- // beforeSnap used to supply it). Pass scheduledBeforeImages but filter out
10966
- // the pasted rows — their pre-schedule image would flip them to `updated`.
10967
- scheduledBeforeImages: new Map(
10968
- [...scheduledBeforeImages].filter(
10969
- ([schedId]) => !createdIdSet.has(String(schedId))
10970
- )
10971
- ),
10972
11183
  links: linkChanges,
10973
11184
  hoursPerDay: sector.hoursPerDay
10974
11185
  })
@@ -11103,10 +11314,8 @@ function collectIncidentLinkIds(activityIds, adapter) {
11103
11314
  }
11104
11315
 
11105
11316
  // src/internal/hierarchy/parent-demotion.ts
11106
- function restoredPromotionFields(parent) {
11107
- const restore = parent.promotionRestore;
11108
- if (typeof restore !== "object" || restore === null) return null;
11109
- if (restore.type !== "task" && restore.type !== "milestone") return null;
11317
+ function restoredPromotionFields(restore) {
11318
+ if (!restore) return null;
11110
11319
  return {
11111
11320
  type: restore.type,
11112
11321
  startDate: new Date(restore.startDate),
@@ -11115,16 +11324,20 @@ function restoredPromotionFields(parent) {
11115
11324
  expectedProgressBaseline: restore.expectedProgressBaseline,
11116
11325
  constraintType: restore.constraintType,
11117
11326
  constraintDate: restore.constraintDate ? new Date(restore.constraintDate) : null,
11118
- progress: restore.progress,
11119
- promotionRestore: null
11327
+ progress: restore.progress
11120
11328
  };
11121
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
+ }
11122
11335
  function buildParentDemotionMutations(input) {
11123
11336
  if (input.remainingChildIds.length > 0) return null;
11124
11337
  const parent = input.parent;
11125
11338
  const canonicalDuration = input.defaultDurationHours;
11126
11339
  const hasCanonicalDefaults = Number.isFinite(canonicalDuration);
11127
- const fields = restoredPromotionFields(parent) ?? (hasCanonicalDefaults ? {
11340
+ const fields = restoredPromotionFields(input.promotionSnapshot) ?? (hasCanonicalDefaults ? {
11128
11341
  type: DEMOTION_TARGET_TYPE,
11129
11342
  durationHours: canonicalDuration,
11130
11343
  progress: 0,
@@ -11132,7 +11345,11 @@ function buildParentDemotionMutations(input) {
11132
11345
  constraintType: "asap",
11133
11346
  constraintDate: null,
11134
11347
  hasNewActivities: false,
11135
- promotionRestore: null
11348
+ ...canonicalEndDateField(
11349
+ parent,
11350
+ canonicalDuration,
11351
+ input.computeEndDate
11352
+ )
11136
11353
  } : { type: DEMOTION_TARGET_TYPE });
11137
11354
  if (input.idsRemoved && input.idsRemoved.size > 0) {
11138
11355
  const existing = Array.isArray(parent.newActivityIds) ? parent.newActivityIds : [];
@@ -11229,6 +11446,8 @@ async function dispatchActivityDelete(action, options, deps) {
11229
11446
  parent,
11230
11447
  remainingChildIds: remainingChildren.map(String),
11231
11448
  defaultDurationHours: sector.hoursPerDay,
11449
+ promotionSnapshot: adapter.getPromotionSnapshot(parentId),
11450
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({ startDate, durationHours, task: parent }),
11232
11451
  idsRemoved: toDelete
11233
11452
  });
11234
11453
  if (!demotion) {
@@ -11264,7 +11483,7 @@ async function dispatchActivityDelete(action, options, deps) {
11264
11483
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11265
11484
  beforeSnap.set(shift.activityId, beforeImage);
11266
11485
  }
11267
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11486
+ const { scheduledIds } = await runPostMutation(
11268
11487
  {
11269
11488
  adapter,
11270
11489
  scheduler,
@@ -11301,7 +11520,6 @@ async function dispatchActivityDelete(action, options, deps) {
11301
11520
  beforeSnap,
11302
11521
  touchedIds,
11303
11522
  scheduledIds,
11304
- scheduledBeforeImages,
11305
11523
  sirDetection: "after-diff",
11306
11524
  links: buildLinkDeletions(beforeLinks),
11307
11525
  trackingEvents: [trackingEvent],
@@ -11473,6 +11691,12 @@ async function dispatchActivityMove(action, options, deps) {
11473
11691
  for (const [key, value] of Object.entries(promotion.fields)) {
11474
11692
  setActivityFieldDynamic(adapter, promotion.parentId, key, value);
11475
11693
  }
11694
+ if (promotion.promotionSnapshot) {
11695
+ adapter.setPromotionSnapshot(
11696
+ promotion.parentId,
11697
+ promotion.promotionSnapshot
11698
+ );
11699
+ }
11476
11700
  if (promotion.fields.customId === null) {
11477
11701
  releaseClearedCustomId(oldNewParentCustomId, deps.customIdTracker);
11478
11702
  }
@@ -11487,6 +11711,12 @@ async function dispatchActivityMove(action, options, deps) {
11487
11711
  parent: oldParentActivity,
11488
11712
  remainingChildIds: remaining.map(String),
11489
11713
  defaultDurationHours: sector.hoursPerDay,
11714
+ promotionSnapshot: adapter.getPromotionSnapshot(oldParentKey),
11715
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({
11716
+ startDate,
11717
+ durationHours,
11718
+ task: oldParentActivity
11719
+ }),
11490
11720
  idsRemoved: /* @__PURE__ */ new Set([action.activityId])
11491
11721
  });
11492
11722
  if (demotion) {
@@ -11526,7 +11756,8 @@ async function dispatchActivityMove(action, options, deps) {
11526
11756
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11527
11757
  beforeSnap.set(shift.activityId, beforeImage);
11528
11758
  }
11529
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11759
+ const sourceParentDirtyIds = parentChanged && oldParentKey !== ROOT_PARENT_ID ? adapter.getChildren(oldParentKey).map(String) : [];
11760
+ const { scheduledIds } = await runPostMutation(
11530
11761
  {
11531
11762
  adapter,
11532
11763
  scheduler,
@@ -11536,9 +11767,14 @@ async function dispatchActivityMove(action, options, deps) {
11536
11767
  {
11537
11768
  action,
11538
11769
  autoscheduleFrom: action.activityId,
11539
- recomputeParentsFrom: [action.activityId, action.parentId],
11770
+ recomputeParentsFrom: [
11771
+ action.activityId,
11772
+ action.parentId,
11773
+ ...sourceParentDirtyIds
11774
+ ],
11540
11775
  now: deps.now,
11541
- options
11776
+ options,
11777
+ recomputeParentProgress: parentChanged
11542
11778
  }
11543
11779
  );
11544
11780
  const invalidLinkIds = parentChanged ? collectInvalidLinkIdsForCycle([action.activityId], adapter) : [];
@@ -11562,7 +11798,6 @@ async function dispatchActivityMove(action, options, deps) {
11562
11798
  beforeSnap,
11563
11799
  touchedIds: touched,
11564
11800
  scheduledIds,
11565
- scheduledBeforeImages,
11566
11801
  sirDetection: "before-diff",
11567
11802
  trackingEvents: [trackingEvent],
11568
11803
  hoursPerDay: sector.hoursPerDay
@@ -11652,6 +11887,12 @@ async function dispatchActivityIndent(action, options, deps) {
11652
11887
  for (const [key, value] of Object.entries(promotion.fields)) {
11653
11888
  setActivityFieldDynamic(adapter, promotion.parentId, key, value);
11654
11889
  }
11890
+ if (promotion.promotionSnapshot) {
11891
+ adapter.setPromotionSnapshot(
11892
+ promotion.parentId,
11893
+ promotion.promotionSnapshot
11894
+ );
11895
+ }
11655
11896
  if (!wasTrackedAsNewChild) {
11656
11897
  adapter.setActivityField(
11657
11898
  newParentId,
@@ -11697,7 +11938,7 @@ async function dispatchActivityIndent(action, options, deps) {
11697
11938
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11698
11939
  beforeSnap.set(shift.activityId, beforeImage);
11699
11940
  }
11700
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11941
+ const { scheduledIds } = await runPostMutation(
11701
11942
  {
11702
11943
  adapter,
11703
11944
  scheduler,
@@ -11754,12 +11995,21 @@ async function dispatchActivityIndent(action, options, deps) {
11754
11995
  }
11755
11996
  return {
11756
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
+ }),
11757
12008
  changes: assembleChangeSet(adapter, {
11758
12009
  source: action,
11759
12010
  beforeSnap,
11760
12011
  touchedIds: touched,
11761
12012
  scheduledIds,
11762
- scheduledBeforeImages,
11763
12013
  trackingEvents: [trackingEvent],
11764
12014
  hoursPerDay: sector.hoursPerDay
11765
12015
  })
@@ -11881,6 +12131,12 @@ async function dispatchActivityOutdent(action, options, deps) {
11881
12131
  parent: parentActivity,
11882
12132
  remainingChildIds: remaining.map(String),
11883
12133
  defaultDurationHours: sector.hoursPerDay,
12134
+ promotionSnapshot: adapter.getPromotionSnapshot(oldParentKey),
12135
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({
12136
+ startDate,
12137
+ durationHours,
12138
+ task: parentActivity
12139
+ }),
11884
12140
  idsRemoved: new Set(
11885
12141
  planned.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
11886
12142
  )
@@ -11926,7 +12182,7 @@ async function dispatchActivityOutdent(action, options, deps) {
11926
12182
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11927
12183
  beforeSnap.set(shift.activityId, beforeImage);
11928
12184
  }
11929
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
12185
+ const { scheduledIds } = await runPostMutation(
11930
12186
  {
11931
12187
  adapter,
11932
12188
  scheduler,
@@ -11959,12 +12215,21 @@ async function dispatchActivityOutdent(action, options, deps) {
11959
12215
  }
11960
12216
  return {
11961
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
+ }),
11962
12228
  changes: assembleChangeSet(adapter, {
11963
12229
  source: action,
11964
12230
  beforeSnap,
11965
12231
  touchedIds: touched,
11966
12232
  scheduledIds,
11967
- scheduledBeforeImages,
11968
12233
  trackingEvents: [trackingEvent],
11969
12234
  hoursPerDay: sector.hoursPerDay
11970
12235
  })
@@ -12056,11 +12321,15 @@ async function dispatchDatesBatch(action, options, deps) {
12056
12321
  mergeBeforeSnapshots(beforeSnap, editTouched, deps);
12057
12322
  for (const id of editTouched) touchedIds.add(id);
12058
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;
12059
12327
  applyFieldChanges(adapter, edit.activityId, outcome.changes);
12060
12328
  runPostProcessorsOnAdapter(
12061
12329
  edit.activityId,
12062
12330
  outcome.changes.postProcessors ?? [],
12063
- adapter
12331
+ adapter,
12332
+ preEditSnapshot
12064
12333
  );
12065
12334
  linkChanges.push(
12066
12335
  ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
@@ -12195,6 +12464,7 @@ async function dispatchBulkEdit(action, options, deps) {
12195
12464
  const verdicts = [];
12196
12465
  const beforeSnap = /* @__PURE__ */ new Map();
12197
12466
  const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12467
+ const explicitDateEditActivities = /* @__PURE__ */ new Set();
12198
12468
  const touchedIds = /* @__PURE__ */ new Set();
12199
12469
  const trackingEvents = [];
12200
12470
  const linkChanges = [];
@@ -12213,14 +12483,21 @@ async function dispatchBulkEdit(action, options, deps) {
12213
12483
  mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12214
12484
  for (const touchedId of editTouched) touchedIds.add(touchedId);
12215
12485
  recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12486
+ if (isExplicitConstraintDateEdit(edit.column)) {
12487
+ explicitDateEditActivities.add(String(edit.activityId));
12488
+ }
12216
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;
12217
12493
  const customIdBeforeApply = readLiveCustomId(edit, deps);
12218
12494
  const changes = outcome.changes;
12219
12495
  applyFieldChanges(adapter, edit.activityId, changes);
12220
12496
  runPostProcessorsOnAdapter(
12221
12497
  edit.activityId,
12222
12498
  outcome.changes.postProcessors ?? [],
12223
- adapter
12499
+ adapter,
12500
+ preEditSnapshot
12224
12501
  );
12225
12502
  syncCustomIdTracker(edit, customIdBeforeApply, deps);
12226
12503
  linkChanges.push(
@@ -12242,7 +12519,7 @@ async function dispatchBulkEdit(action, options, deps) {
12242
12519
  options
12243
12520
  };
12244
12521
  const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12245
- const { scheduledIds, scheduledBeforeImages } = await (batchHasConstraintEdits ? settleConstraintEdits(
12522
+ const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
12246
12523
  {
12247
12524
  adapter,
12248
12525
  scheduler,
@@ -12251,12 +12528,12 @@ async function dispatchBulkEdit(action, options, deps) {
12251
12528
  },
12252
12529
  {
12253
12530
  postMutationArgs,
12254
- revertTargets: [...constraintPriorsByActivity].map(
12255
- ([activityId, priorConstraintDate]) => ({
12256
- activityId,
12257
- priorConstraintDate
12258
- })
12259
- ),
12531
+ revertTargets: [...constraintPriorsByActivity].filter(
12532
+ ([activityId]) => !explicitDateEditActivities.has(activityId)
12533
+ ).map(([activityId, priorConstraintDate]) => ({
12534
+ activityId,
12535
+ priorConstraintDate
12536
+ })),
12260
12537
  revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12261
12538
  activityId,
12262
12539
  priorConstraintDate,
@@ -12284,7 +12561,6 @@ async function dispatchBulkEdit(action, options, deps) {
12284
12561
  // Los dependientes que mueve el autoscheduler no están en beforeSnap;
12285
12562
  // sin su before-image el diff los marca FALSE-CREATED y el undo los
12286
12563
  // borra (fix 4bbccbc de inline-edit).
12287
- scheduledBeforeImages,
12288
12564
  sirDetection: "before-diff",
12289
12565
  links: linkChanges,
12290
12566
  trackingEvents,
@@ -15883,8 +16159,15 @@ var WriteCapture = class {
15883
16159
  /** Pre-applyResults clone of an activity the autoscheduler will move (first-wins). */
15884
16160
  noteScheduledBefore(activityId, current) {
15885
16161
  const journal = this._journal;
15886
- if (!journal || journal.scheduledBefore.has(activityId)) return;
15887
- 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);
15888
16171
  }
15889
16172
  /** Field write on a link — pre-mutation clone on first write per link id. */
15890
16173
  noteLinkField(linkId, current) {
@@ -15898,20 +16181,6 @@ var WriteCapture = class {
15898
16181
  });
15899
16182
  }
15900
16183
  };
15901
- function cloneCoreActivity(activity) {
15902
- return {
15903
- ...activity,
15904
- startDate: new Date(activity.startDate),
15905
- endDate: new Date(activity.endDate),
15906
- constraintDate: activity.constraintDate ? new Date(activity.constraintDate) : null,
15907
- dateOrigin: activity.dateOrigin ? new Date(activity.dateOrigin) : null,
15908
- newActivityIds: [...activity.newActivityIds],
15909
- pendingRequestIds: [...activity.pendingRequestIds],
15910
- responsableIds: [...activity.responsableIds],
15911
- tagIds: [...activity.tagIds],
15912
- baselinePoints: [...activity.baselinePoints]
15913
- };
15914
- }
15915
16184
  function cloneLink(link) {
15916
16185
  return { ...link };
15917
16186
  }
@@ -16165,6 +16434,13 @@ var ScheduleState = class {
16165
16434
  // whether a soft constraint repositioned the activity. Never persisted,
16166
16435
  // never in the ChangeSet — not domain data on CoreActivity.
16167
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();
16168
16444
  _links = /* @__PURE__ */ new Map();
16169
16445
  _outgoing = /* @__PURE__ */ new Map();
16170
16446
  _incoming = /* @__PURE__ */ new Map();
@@ -16180,6 +16456,7 @@ var ScheduleState = class {
16180
16456
  _viewState = new ViewStateStore();
16181
16457
  _viewStateBefore = null;
16182
16458
  _lastStartDateBefore = null;
16459
+ _promotionSnapshotBefore = null;
16183
16460
  // Calendar arithmetic + in-dispatch memoization. Assigned in the
16184
16461
  // constructor once the calendar reader is built. See `CalendarCalculator`.
16185
16462
  _calendar;
@@ -16264,6 +16541,19 @@ var ScheduleState = class {
16264
16541
  this._captureLastStartDateOnce();
16265
16542
  this._lastStartDate.set(String(activityId), startDate);
16266
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
+ }
16267
16557
  _captureLastStartDateOnce() {
16268
16558
  if (this._writeCapture.peek() === null) return;
16269
16559
  if (this._lastStartDateBefore === null) {
@@ -16380,6 +16670,7 @@ var ScheduleState = class {
16380
16670
  this._writeCapture.begin();
16381
16671
  this._viewStateBefore = null;
16382
16672
  this._lastStartDateBefore = null;
16673
+ this._promotionSnapshotBefore = null;
16383
16674
  }
16384
16675
  /** The live journal, or null when no capture is active. */
16385
16676
  peekWriteCapture() {
@@ -16390,6 +16681,7 @@ var ScheduleState = class {
16390
16681
  this._writeCapture.end();
16391
16682
  this._viewStateBefore = null;
16392
16683
  this._lastStartDateBefore = null;
16684
+ this._promotionSnapshotBefore = null;
16393
16685
  }
16394
16686
  // -- Replay-specific mutators --------------------------------------------
16395
16687
  setActivityField(activityId, field, value) {
@@ -16541,6 +16833,9 @@ var ScheduleState = class {
16541
16833
  if (this._viewStateBefore !== null) {
16542
16834
  this._viewState.restore(this._viewStateBefore);
16543
16835
  }
16836
+ if (this._promotionSnapshotBefore !== null) {
16837
+ this._promotionSnapshot = this._promotionSnapshotBefore;
16838
+ }
16544
16839
  if (this._lastStartDateBefore !== null) {
16545
16840
  this._lastStartDate = this._lastStartDateBefore;
16546
16841
  }
@@ -16673,6 +16968,13 @@ function readSelectedActivityIds(state) {
16673
16968
  return state.checkedIds().map(String);
16674
16969
  }
16675
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;
16977
+ }
16676
16978
  return state.getChildren(parentId).length > 0;
16677
16979
  }
16678
16980
  function readLink(state, id) {
@@ -16815,6 +17117,7 @@ function buildInverseChangeSet(state, entry, side) {
16815
17117
  activities.push({
16816
17118
  id: activityId,
16817
17119
  kind: "updated",
17120
+ fields: historyFields(change.fields, side),
16818
17121
  after: snapshot ? structuredCloneActivity(snapshot) : null
16819
17122
  });
16820
17123
  } else if (change.kind === "deleted") {
@@ -16848,6 +17151,7 @@ function buildInverseChangeSet(state, entry, side) {
16848
17151
  links.push({
16849
17152
  id: linkId,
16850
17153
  kind: "updated",
17154
+ fields: historyFields(change.fields, side),
16851
17155
  after: lk ? snapshotToLink(lk) : null
16852
17156
  });
16853
17157
  } else if (change.kind === "deleted") {
@@ -16887,6 +17191,21 @@ function buildInverseChangeSet(state, entry, side) {
16887
17191
  viewState
16888
17192
  };
16889
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
+ }
16890
17209
  function invertViewStateChange(change) {
16891
17210
  return {
16892
17211
  activityId: change.activityId,
@@ -17425,8 +17744,7 @@ function parseActivity(raw, context) {
17425
17744
  expectedProgress: null,
17426
17745
  expectedProgressBaseline: null,
17427
17746
  status: null,
17428
- criticalPath: null,
17429
- promotionRestore: null
17747
+ criticalPath: null
17430
17748
  };
17431
17749
  }
17432
17750
  function forceRootProjectToRoot(activities) {
@@ -18862,9 +19180,11 @@ var ScheduleCore = class {
18862
19180
  * ineligible to commit.
18863
19181
  */
18864
19182
  recomputeCriticalPath() {
18865
- return this._enqueue(async () => ({
19183
+ const operation = this._enqueue(async () => ({
18866
19184
  job: this._startCriticalPathForCurrentRevision()
18867
19185
  })).then(({ job }) => job);
19186
+ this._criticalPathReady = operation;
19187
+ return operation;
18868
19188
  }
18869
19189
  isCriticalPathSettled() {
18870
19190
  return this._criticalPathRevision === this._scheduleRevision && this._activeCriticalPath === null;
@@ -18998,11 +19318,11 @@ var ScheduleCore = class {
18998
19318
  if (this.coreRuntime.clock) {
18999
19319
  const now = endOfLocalDay(this.coreRuntime.clock());
19000
19320
  applyExpectedProgressLive(state, now);
19001
- let hasActiveBaseline = false;
19321
+ let hasActiveBaseline2 = false;
19002
19322
  state.forEachActivity((activity) => {
19003
- hasActiveBaseline ||= getActiveBaseline(activity) !== null;
19323
+ hasActiveBaseline2 ||= getActiveBaseline(activity) !== null;
19004
19324
  });
19005
- if (hasActiveBaseline) {
19325
+ if (hasActiveBaseline2) {
19006
19326
  runExpectedProgressBase(
19007
19327
  state,
19008
19328
  now,