@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.cjs CHANGED
@@ -926,7 +926,8 @@ function expandParentLinks(links, adapter) {
926
926
  targetIds,
927
927
  link,
928
928
  adapter,
929
- virtualIdCounter
929
+ virtualIdCounter,
930
+ linksByActivity
930
931
  );
931
932
  }
932
933
  return expanded;
@@ -943,19 +944,96 @@ function expandedEndpoints(activityId, summary, adapter, linksByActivity, source
943
944
  sourceSide
944
945
  );
945
946
  }
946
- function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, adapter, virtualIdCounter) {
947
+ function deriveSummaryBoundsWithDates(summaryId, adapter, resolveDates) {
948
+ const leaves = getLeafDescendants(summaryId, adapter);
949
+ if (leaves.length === 0) return null;
950
+ let earliestStart = null;
951
+ let latestEnd = null;
952
+ for (const leafId of leaves) {
953
+ const leafDates = resolveDates(leafId);
954
+ if (!leafDates) continue;
955
+ const startsEarlier = earliestStart === null || leafDates.startDate < earliestStart;
956
+ if (startsEarlier) earliestStart = leafDates.startDate;
957
+ const endsLater = latestEnd === null || leafDates.endDate > latestEnd;
958
+ if (endsLater) latestEnd = leafDates.endDate;
959
+ }
960
+ if (earliestStart === null || latestEnd === null) return null;
961
+ return { startDate: earliestStart, endDate: latestEnd };
962
+ }
963
+ function deriveSummaryBounds(summaryId, adapter) {
964
+ return deriveSummaryBoundsWithDates(summaryId, adapter, (leafId) => {
965
+ const leaf = adapter.getActivity(leafId);
966
+ if (!leaf) return null;
967
+ return { startDate: leaf.startDate, endDate: leaf.endDate };
968
+ });
969
+ }
970
+ function endpointSpanHours(endpoint, bounds, adapter) {
971
+ if (!endpoint) return 0;
972
+ if (!bounds) return endpoint.durationHours;
973
+ return adapter.calculateDuration({
974
+ startDate: bounds.startDate,
975
+ endDate: bounds.endDate,
976
+ task: endpoint
977
+ });
978
+ }
979
+ function sourceLagForLeaf(leafId, source, sourceBounds, adapter) {
980
+ if (!source) return 0;
981
+ const leaf = adapter.getActivity(leafId);
982
+ if (!sourceBounds || !leaf) return -source.durationHours;
983
+ return -adapter.calculateDuration({
984
+ startDate: sourceBounds.startDate,
985
+ endDate: leaf.endDate,
986
+ task: source
987
+ });
988
+ }
989
+ function hasIncomingRawLink(leafId, linksByActivity) {
990
+ const leafKey = String(leafId);
991
+ const bucket = linksByActivity.get(leafKey) ?? [];
992
+ return bucket.some((rawLink) => String(rawLink.target) === leafKey);
993
+ }
994
+ function targetOffsetForLeaf(leafId, targetStart, source, adapter, linksByActivity) {
995
+ if (hasIncomingRawLink(leafId, linksByActivity)) return 0;
996
+ const leaf = adapter.getActivity(leafId);
997
+ if (!targetStart || !leaf || !source) return 0;
998
+ return adapter.calculateDuration({
999
+ startDate: targetStart,
1000
+ endDate: leaf.startDate,
1001
+ task: source
1002
+ });
1003
+ }
1004
+ function resolveExpansionAnchors(originalLink, adapter) {
947
1005
  const source = adapter.getActivity(originalLink.source);
948
1006
  const target = adapter.getActivity(originalLink.target);
949
- const targetStart = target?.startDate;
950
- const userLag = originalLink.lag ?? 0;
1007
+ const sourceBounds = deriveSummaryBounds(originalLink.source, adapter);
1008
+ const targetBounds = deriveSummaryBounds(originalLink.target, adapter);
951
1009
  const sourceEndpointIsStart = originalLink.type === LINK_TYPE.START_TO_START || originalLink.type === LINK_TYPE.START_TO_FINISH;
952
1010
  const targetEndpointIsFinish = originalLink.type === LINK_TYPE.FINISH_TO_FINISH || originalLink.type === LINK_TYPE.START_TO_FINISH;
953
- const sourceLag = sourceEndpointIsStart ? -(source?.durationHours ?? 0) : 0;
954
- const targetLag = targetEndpointIsFinish ? -(target?.durationHours ?? 0) : 0;
1011
+ return {
1012
+ source,
1013
+ sourceBounds,
1014
+ targetStart: targetBounds?.startDate ?? target?.startDate,
1015
+ userLag: originalLink.lag ?? 0,
1016
+ sourceEndpointIsStart,
1017
+ targetLag: targetEndpointIsFinish ? -endpointSpanHours(target, targetBounds, adapter) : 0,
1018
+ summarySourceId: originalLink.type === LINK_TYPE.START_TO_START && sourceBounds ? originalLink.source : void 0
1019
+ };
1020
+ }
1021
+ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, adapter, virtualIdCounter, linksByActivity) {
1022
+ const anchors = resolveExpansionAnchors(originalLink, adapter);
1023
+ const { source, sourceBounds, targetStart, userLag } = anchors;
1024
+ const { sourceEndpointIsStart, targetLag, summarySourceId } = anchors;
955
1025
  let counter = virtualIdCounter;
956
1026
  for (const srcId of sourceIds) {
1027
+ const sourceLag = sourceEndpointIsStart ? sourceLagForLeaf(srcId, source, sourceBounds, adapter) : 0;
957
1028
  for (const tgtId of leafIds) {
958
1029
  if (String(srcId) === String(tgtId)) continue;
1030
+ const offTarget = targetOffsetForLeaf(
1031
+ tgtId,
1032
+ targetStart,
1033
+ source,
1034
+ adapter,
1035
+ linksByActivity
1036
+ );
959
1037
  expanded.push(
960
1038
  buildVirtualLink(
961
1039
  originalLink,
@@ -964,10 +1042,9 @@ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, a
964
1042
  counter,
965
1043
  sourceLag,
966
1044
  targetLag,
967
- userLag,
968
- source,
969
- targetStart,
970
- adapter
1045
+ userLag + offTarget,
1046
+ summarySourceId,
1047
+ offTarget !== 0 && targetLag === 0 ? originalLink.target : void 0
971
1048
  )
972
1049
  );
973
1050
  counter += 1;
@@ -975,13 +1052,7 @@ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, a
975
1052
  }
976
1053
  return counter;
977
1054
  }
978
- function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag, targetLag, userLag, source, targetStart, adapter) {
979
- const leaf = adapter.getActivity(targetId);
980
- const offTarget = targetStart && leaf && source ? adapter.calculateDuration({
981
- startDate: targetStart,
982
- endDate: leaf.startDate,
983
- task: source
984
- }) : 0;
1055
+ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag, targetLag, trueLag, summarySourceId, offsetSummaryTargetId) {
985
1056
  return {
986
1057
  id: `virtual_${originalLink.id}_${counter}`,
987
1058
  source: sourceId,
@@ -990,7 +1061,9 @@ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag,
990
1061
  lag: originalLink.lag,
991
1062
  _sourceLag: sourceLag,
992
1063
  _targetLag: targetLag,
993
- _trueLag: userLag + offTarget
1064
+ _trueLag: trueLag,
1065
+ ...summarySourceId === void 0 ? {} : { _summarySourceId: summarySourceId },
1066
+ ...offsetSummaryTargetId === void 0 ? {} : { _summaryTargetId: offsetSummaryTargetId }
994
1067
  };
995
1068
  }
996
1069
 
@@ -1122,33 +1195,49 @@ function normalize(predecessor, successor, link) {
1122
1195
  };
1123
1196
  }
1124
1197
  }
1125
- function calculateSuccessorStartFromLink(predecessor, successor, link, adapter) {
1126
- const { sourceLag, targetLag, trueLag, isFS, isFF } = normalize(
1127
- predecessor,
1128
- successor,
1198
+ function summaryAnchoredSourceLag(link, predecessor, adapter) {
1199
+ if (link._summarySourceId === void 0) return null;
1200
+ const summarySource = adapter.getActivity(link._summarySourceId);
1201
+ if (!summarySource) return null;
1202
+ const summaryBounds = deriveSummaryBounds(link._summarySourceId, adapter);
1203
+ if (!summaryBounds) return null;
1204
+ return -adapter.calculateDuration({
1205
+ startDate: summaryBounds.startDate,
1206
+ endDate: predecessor.endDate,
1207
+ task: summarySource
1208
+ });
1209
+ }
1210
+ function noLagSuccessorStart(predecessor, successor, link, adapter, isEndAnchoredType) {
1211
+ const snapsToPast = successor.durationHours === 0 && isEndAnchoredType;
1212
+ return {
1213
+ successorStart: adapter.getClosestWorkTime({
1214
+ date: predecessor.endDate,
1215
+ dir: snapsToPast ? "past" : "future",
1216
+ task: successor
1217
+ }),
1129
1218
  link
1130
- );
1131
- const hasLag = sourceLag !== 0 || targetLag !== 0 || trueLag !== 0;
1219
+ };
1220
+ }
1221
+ function calculateSuccessorStartFromLink(predecessor, successor, link, adapter) {
1222
+ const {
1223
+ sourceLag: frozenSourceLag,
1224
+ targetLag,
1225
+ trueLag,
1226
+ isFS,
1227
+ isFF
1228
+ } = normalize(predecessor, successor, link);
1229
+ const sourceLag = summaryAnchoredSourceLag(link, predecessor, adapter) ?? frozenSourceLag;
1230
+ const totalLag = sourceLag + targetLag + trueLag;
1231
+ const isZeroDurationSuccessor = successor.durationHours === 0;
1232
+ const hasLag = isZeroDurationSuccessor ? totalLag !== 0 : sourceLag !== 0 || targetLag !== 0 || trueLag !== 0;
1132
1233
  if (!hasLag) {
1133
- const isFForFS = isFS || isFF;
1134
- if (successor.durationHours === 0 && isFForFS) {
1135
- return {
1136
- successorStart: adapter.getClosestWorkTime({
1137
- date: predecessor.endDate,
1138
- dir: "past",
1139
- task: successor
1140
- }),
1141
- link
1142
- };
1143
- }
1144
- return {
1145
- successorStart: adapter.getClosestWorkTime({
1146
- date: predecessor.endDate,
1147
- dir: "future",
1148
- task: successor
1149
- }),
1150
- link
1151
- };
1234
+ return noLagSuccessorStart(
1235
+ predecessor,
1236
+ successor,
1237
+ link,
1238
+ adapter,
1239
+ isFS || isFF
1240
+ );
1152
1241
  }
1153
1242
  const baseTask = isFS ? successor : predecessor;
1154
1243
  let date2 = adapter.getClosestWorkTime({
@@ -1369,10 +1458,12 @@ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
1369
1458
  const incoming = incomingLinks.get(String(activityId)) ?? [];
1370
1459
  let maxStart = null;
1371
1460
  let drivingLinkId = null;
1461
+ let drivingOffsetIsStale = false;
1372
1462
  for (const link of incoming) {
1373
1463
  const predecessorSnap = adapter.getActivity(link.source);
1374
1464
  if (!predecessorSnap) continue;
1375
1465
  const predDates = getActivityDates(link.source);
1466
+ const predecessorMovedThisRun = predDates.startDate.getTime() !== predecessorSnap.startDate.getTime() || predDates.endDate.getTime() !== predecessorSnap.endDate.getTime();
1376
1467
  const effectivePred = {
1377
1468
  ...predecessorSnap,
1378
1469
  startDate: predDates.startDate,
@@ -1402,8 +1493,13 @@ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
1402
1493
  if (!maxStart || result.successorStart > maxStart) {
1403
1494
  maxStart = result.successorStart;
1404
1495
  drivingLinkId = String(link.id);
1496
+ drivingOffsetIsStale = link._summaryTargetId !== void 0 && !predecessorMovedThisRun;
1405
1497
  }
1406
1498
  }
1499
+ const staleOffsetWouldPullBack = maxStart !== null && drivingOffsetIsStale && maxStart < activity.startDate;
1500
+ if (staleOffsetWouldPullBack) {
1501
+ maxStart = new Date(activity.startDate);
1502
+ }
1407
1503
  const existing = plans.get(activityId);
1408
1504
  const plan = existing ? { ...existing } : {
1409
1505
  activityId,
@@ -1571,31 +1667,33 @@ async function alapPass(reversedIds, links, asapPlans, adapter, _options, isCurr
1571
1667
  latestEnd = boundary;
1572
1668
  }
1573
1669
  }
1670
+ const incomingForKind = incomingLinks.get(String(activityId)) ?? [];
1671
+ const isFinishMarker = incomingForKind.some(
1672
+ (incomingLink) => incomingLink.type === LINK_TYPE.FINISH_TO_START || incomingLink.type === LINK_TYPE.FINISH_TO_FINISH
1673
+ );
1674
+ const latestEndAtDayStart = new Date(
1675
+ Date.UTC(
1676
+ latestEnd.getUTCFullYear(),
1677
+ latestEnd.getUTCMonth(),
1678
+ latestEnd.getUTCDate()
1679
+ )
1680
+ );
1681
+ const finishMarkerEnd = () => adapter.getClosestWorkTime({
1682
+ date: latestEndAtDayStart,
1683
+ dir: "past",
1684
+ task: activity
1685
+ });
1686
+ const latestEndLandsInsideWorkingDay = adapter.getClosestWorkTime({ date: latestEnd, dir: "future", task: activity }).getTime() === latestEnd.getTime();
1574
1687
  let snappedEnd;
1575
1688
  if (activity.durationHours === 0 && successorAnchor === null) {
1576
- snappedEnd = adapter.getClosestWorkTime({
1689
+ snappedEnd = isFinishMarker && latestEndLandsInsideWorkingDay ? finishMarkerEnd() : adapter.getClosestWorkTime({
1577
1690
  date: latestEnd,
1578
1691
  dir: "past",
1579
1692
  task: activity
1580
1693
  });
1581
1694
  } else if (activity.durationHours === 0) {
1582
- const incoming = incomingLinks.get(String(activityId)) ?? [];
1583
- const isFinishKind = incoming.some(
1584
- (l) => l.type === LINK_TYPE.FINISH_TO_START || l.type === LINK_TYPE.FINISH_TO_FINISH
1585
- );
1586
- if (isFinishKind) {
1587
- const dayStart = new Date(
1588
- Date.UTC(
1589
- latestEnd.getUTCFullYear(),
1590
- latestEnd.getUTCMonth(),
1591
- latestEnd.getUTCDate()
1592
- )
1593
- );
1594
- snappedEnd = adapter.getClosestWorkTime({
1595
- date: dayStart,
1596
- dir: "past",
1597
- task: activity
1598
- });
1695
+ if (isFinishMarker) {
1696
+ snappedEnd = finishMarkerEnd();
1599
1697
  } else {
1600
1698
  snappedEnd = adapter.getClosestWorkTime({
1601
1699
  date: latestEnd,
@@ -1628,10 +1726,16 @@ async function alapPass(reversedIds, links, asapPlans, adapter, _options, isCurr
1628
1726
  plan.endDate = snappedEnd;
1629
1727
  plan.drivingLinkId = null;
1630
1728
  plan.kind = "alap";
1729
+ const predecessorFloor = plan.earliestSchedulingStart ?? null;
1631
1730
  plan.earliestSchedulingStart = null;
1632
1731
  plan.earliestSchedulingEnd = null;
1633
- plan.latestSchedulingStart = snappedStart;
1634
- plan.latestSchedulingEnd = snappedEnd;
1732
+ const alapTarget = predecessorFloor !== null && predecessorFloor.getTime() > snappedStart.getTime() ? predecessorFloor : snappedStart;
1733
+ plan.latestSchedulingStart = alapTarget;
1734
+ plan.latestSchedulingEnd = alapTarget === snappedStart ? snappedEnd : adapter.calculateEndDate({
1735
+ startDate: alapTarget,
1736
+ durationHours: activity.durationHours,
1737
+ task: activity
1738
+ });
1635
1739
  limitPlanDates(activity, plan, adapter);
1636
1740
  plans.set(activityId, plan);
1637
1741
  }
@@ -2066,18 +2170,9 @@ var AutoScheduler = class {
2066
2170
  }
2067
2171
  const phaseScope = startPhase("schedule.scoping");
2068
2172
  const allIds = this.adapter.getAllIds();
2069
- let scopedLinks = activeLinks;
2070
- let scopedIds = allIds;
2071
- let scopedIdSet = null;
2072
- if (options.triggerId !== void 0 && options.triggerId !== null) {
2073
- const group = findConnectedGroupForTrigger(
2074
- options.triggerId,
2075
- activeLinks
2076
- );
2077
- scopedLinks = group.links;
2078
- scopedIds = Array.from(group.activityIds);
2079
- scopedIdSet = new Set(scopedIds);
2080
- }
2173
+ const scope = resolveScope(options, activeLinks, this.adapter);
2174
+ const scopedLinks = scope.links;
2175
+ const scopedIdSet = scope.idSet;
2081
2176
  endPhase(phaseScope);
2082
2177
  const phaseTopo = startPhase("schedule.topologicalSort");
2083
2178
  const topoResult = resolveTopoResult(
@@ -2174,6 +2269,34 @@ function endPhase(handle) {
2174
2269
  const elapsed = performance.now() - handle.startedAt;
2175
2270
  console.log(`[scheduler] ${handle.name} ${elapsed.toFixed(1)}ms`);
2176
2271
  }
2272
+ function resolveScope(options, activeLinks, adapter) {
2273
+ const triggerId = options.triggerId;
2274
+ if (triggerId === void 0 || triggerId === null) {
2275
+ return { idSet: null, links: activeLinks };
2276
+ }
2277
+ const group = findConnectedGroupForTrigger(triggerId, activeLinks);
2278
+ return widenScopeToAlapActivities(group, activeLinks, adapter);
2279
+ }
2280
+ function widenScopeToAlapActivities(group, allLinks, adapter) {
2281
+ const idSet = new Set(group.activityIds);
2282
+ const widenedAlapKeys = /* @__PURE__ */ new Set();
2283
+ for (const activity of adapter.getAllActivities()) {
2284
+ const activityKey = String(activity.id);
2285
+ const isOutOfComponentAlap = activity.constraintType === CONSTRAINT_TYPE2.ALAP && !idSet.has(activityKey);
2286
+ if (!isOutOfComponentAlap) continue;
2287
+ widenedAlapKeys.add(activityKey);
2288
+ idSet.add(activityKey);
2289
+ }
2290
+ if (widenedAlapKeys.size === 0) {
2291
+ return { idSet, links: group.links };
2292
+ }
2293
+ const links = [...group.links];
2294
+ for (const link of allLinks) {
2295
+ const touchesWidenedAlap = widenedAlapKeys.has(String(link.source)) || widenedAlapKeys.has(String(link.target));
2296
+ if (touchesWidenedAlap) links.push(link);
2297
+ }
2298
+ return { idSet, links };
2299
+ }
2177
2300
  function resolveTopoResult(cache, allIds, allLinks, scopedIdSet) {
2178
2301
  const fullTopo = cache.get(allIds, allLinks);
2179
2302
  if (fullTopo.hasCycles) {
@@ -2191,9 +2314,10 @@ function resolveTopoResult(cache, allIds, allLinks, scopedIdSet) {
2191
2314
  }
2192
2315
 
2193
2316
  // src/internal/post-processors/adjust-link-lag-on-task-move.ts
2194
- function adjustLinkLagOnTaskMove(targetId, adapter) {
2317
+ function adjustLinkLagOnTaskMove(targetId, adapter, preEditTarget) {
2195
2318
  const target = adapter.getActivity(targetId);
2196
2319
  if (!target) return;
2320
+ const targetForLagMath = preEditTarget ? { ...target, endDate: preEditTarget.endDate } : target;
2197
2321
  const calendarApi = adapter.calendarReader.getCalendar(
2198
2322
  String(target.calendarId)
2199
2323
  );
@@ -2203,9 +2327,11 @@ function adjustLinkLagOnTaskMove(targetId, adapter) {
2203
2327
  const source = adapter.getActivity(link.source);
2204
2328
  if (!source) continue;
2205
2329
  if (source.constraintType === "alap") continue;
2330
+ const isEndAnchoredIntoMilestone = isMilestoneType(target.type) && (link.type === LINK_TYPE.START_TO_FINISH || link.type === LINK_TYPE.FINISH_TO_FINISH);
2331
+ if (isEndAnchoredIntoMilestone) continue;
2206
2332
  const { sourceDate, targetDate } = pickDatesForLinkType(
2207
2333
  source,
2208
- target,
2334
+ targetForLagMath,
2209
2335
  link.type
2210
2336
  );
2211
2337
  const newLag = dateMath.computeLagBetweenTasks(
@@ -2253,14 +2379,15 @@ function computeLeafRealWork(workHours, progress) {
2253
2379
  // src/propagations/upward/parent-bounds.ts
2254
2380
  async function updateParentBoundsFromChildren(adapter, dirtyIds, recomputeProgress = true, options = {}) {
2255
2381
  const parents = dirtyIds ? findAncestorsOfDirty(adapter, dirtyIds) : findAllParents(adapter);
2256
- await recomputeParentsCooperatively(parents, adapter, recomputeProgress, {
2382
+ const progressMode = recomputeProgress ? options.progressMode ?? "freeze-when-no-contribution" : "skip";
2383
+ await recomputeParentsCooperatively(parents, adapter, progressMode, {
2257
2384
  recomputeRealWork: options.recomputeRealWork ?? false
2258
2385
  });
2259
2386
  }
2260
2387
  function recomputeSingleParentDisplayBounds(parentId, adapter) {
2261
- recomputeParentFromChildren(parentId, adapter, false);
2388
+ recomputeParentFromChildren(parentId, adapter, "skip");
2262
2389
  }
2263
- async function recomputeParentsCooperatively(parents, adapter, recomputeProgress = true, options = {
2390
+ async function recomputeParentsCooperatively(parents, adapter, progressMode, options = {
2264
2391
  recomputeRealWork: false
2265
2392
  }) {
2266
2393
  const YIELD_EVERY_N_PARENTS = 200;
@@ -2269,7 +2396,7 @@ async function recomputeParentsCooperatively(parents, adapter, recomputeProgress
2269
2396
  recomputeParentFromChildren(
2270
2397
  parentId,
2271
2398
  adapter,
2272
- recomputeProgress,
2399
+ progressMode,
2273
2400
  options.recomputeRealWork
2274
2401
  );
2275
2402
  processed += 1;
@@ -2319,7 +2446,7 @@ function aggregateChildren(childrenIds, adapter, includeRealWork) {
2319
2446
  }
2320
2447
  return { minStart, maxEnd, realWorkSum, weightedChildren };
2321
2448
  }
2322
- function recomputeParentFromChildren(parentId, adapter, recomputeProgress = true, recomputeRealWork = false) {
2449
+ function recomputeParentFromChildren(parentId, adapter, progressMode = "freeze-when-no-contribution", recomputeRealWork = false) {
2323
2450
  const parent = adapter.getActivity(parentId);
2324
2451
  if (!parent) return;
2325
2452
  const childrenIds = adapter.getChildren(parentId);
@@ -2337,11 +2464,12 @@ function recomputeParentFromChildren(parentId, adapter, recomputeProgress = true
2337
2464
  const progressRollup = computeWeightedProgressRollup(
2338
2465
  aggregate.weightedChildren
2339
2466
  );
2340
- if (recomputeProgress && progressRollup !== null) {
2467
+ const shouldWriteProgress = progressMode !== "skip" && (progressRollup !== null || progressMode === "zero-when-no-contribution");
2468
+ if (shouldWriteProgress) {
2341
2469
  adapter.setActivityField(
2342
2470
  parentId,
2343
2471
  "progress",
2344
- roundProgressPerLevel(progressRollup)
2472
+ roundProgressPerLevel(progressRollup ?? 0)
2345
2473
  );
2346
2474
  }
2347
2475
  if (recomputeRealWork) {
@@ -2403,7 +2531,7 @@ function runRecordLastStartDate(activityId, adapter) {
2403
2531
  const activity = adapter.getActivity(activityId);
2404
2532
  if (activity) adapter.setLastStartDate(activityId, activity.startDate);
2405
2533
  }
2406
- function runPostProcessorsOnAdapter(activityId, processorNames, adapter) {
2534
+ function runPostProcessorsOnAdapter(activityId, processorNames, adapter, preEditActivity) {
2407
2535
  for (const name of processorNames) {
2408
2536
  const fn = name in POST_PROCESSORS ? POST_PROCESSORS[name] : void 0;
2409
2537
  if (!fn) {
@@ -2412,7 +2540,7 @@ function runPostProcessorsOnAdapter(activityId, processorNames, adapter) {
2412
2540
  );
2413
2541
  continue;
2414
2542
  }
2415
- fn(activityId, adapter);
2543
+ fn(activityId, adapter, preEditActivity ?? null);
2416
2544
  }
2417
2545
  }
2418
2546
  function runNoOp(name) {
@@ -2685,10 +2813,7 @@ var durationPipeline = {
2685
2813
  );
2686
2814
  return {
2687
2815
  ...fieldChanges(extraFields, {
2688
- // MAIN treats an ALAP duration edit as a resize from Start: Start is
2689
- // anchored and Finish moves. Running the ALAP pass would instead
2690
- // preserve Finish and pull Start, which is not the observed UI flow.
2691
- autoSchedule: activity.constraintType !== "alap",
2816
+ autoSchedule: true,
2692
2817
  postProcessors: [POST_PROCESSOR.RECORD_LAST_START_DATE]
2693
2818
  }),
2694
2819
  ...constraintCheck.violated ? {
@@ -2850,16 +2975,22 @@ function buildCanonicalRealWorkOverlay(activity, progress, hierarchy) {
2850
2975
  realWorkByActivity.set(activity.id, selfRealWork);
2851
2976
  return realWorkByActivity;
2852
2977
  }
2978
+ function summariesOverSummariesDeepestFirst(activity, hierarchy) {
2979
+ const subtreeIds = [activity.id, ...hierarchy.getDescendantIds(activity.id)];
2980
+ const hasSummaryChild = (parentId) => hierarchy.getChildProgressInfo(parentId).some((child) => hierarchy.hasChildren(child.id));
2981
+ const depthOf3 = (activityId) => hierarchy.getAncestorIds(activityId).length;
2982
+ return subtreeIds.filter((candidateId) => hasSummaryChild(String(candidateId))).map(String).sort((first, second) => depthOf3(second) - depthOf3(first));
2983
+ }
2853
2984
  function buildAncestorRollupCascade(activity, newValue, hierarchy) {
2854
2985
  const overlay = /* @__PURE__ */ new Map();
2855
2986
  overlay.set(activity.id, newValue);
2856
2987
  for (const descendantId of hierarchy.getDescendantIds(activity.id)) {
2857
2988
  overlay.set(descendantId, newValue);
2858
2989
  }
2859
- const chain = [];
2860
- for (const ancestorId of hierarchy.getAncestorIds(activity.id)) {
2861
- chain.push(ancestorId);
2862
- }
2990
+ const chain = [
2991
+ ...summariesOverSummariesDeepestFirst(activity, hierarchy),
2992
+ ...hierarchy.getAncestorIds(activity.id)
2993
+ ];
2863
2994
  const mutations = [];
2864
2995
  for (const id of chain) {
2865
2996
  const rollup = computeWeightedProgress(id, overlay, hierarchy);
@@ -3256,6 +3387,8 @@ function checkIsWorkingDay(newDate, activity, ctx) {
3256
3387
  return null;
3257
3388
  }
3258
3389
  function detectEndDateConstraintWarning(activity, newEndDate) {
3390
+ const cannotViolateStartPin = effectiveConstraintType(activity) === CONSTRAINT_TYPE2.MSO;
3391
+ if (cannotViolateStartPin) return void 0;
3259
3392
  const projectedDate = pickProjectedDateForConstraint(
3260
3393
  effectiveConstraintType(activity),
3261
3394
  activity.startDate,
@@ -3276,7 +3409,8 @@ function detectEndDateConstraintWarning(activity, newEndDate) {
3276
3409
  function buildEndDateExtraFields(activity, newEndDate, ctx, violatesConstraint = false) {
3277
3410
  const fields = {};
3278
3411
  const isMilestone = isMilestoneType(activity.type);
3279
- if (isMilestone && !violatesConstraint) {
3412
+ const seedsMilestoneDate = isMilestone && !violatesConstraint && hasDatelessConstraintType(activity);
3413
+ if (seedsMilestoneDate) {
3280
3414
  Object.assign(fields, setConstraintDate(activity.startDate));
3281
3415
  } else if (violatesConstraint || hasDatelessConstraintType(activity)) {
3282
3416
  Object.assign(
@@ -3286,10 +3420,8 @@ function buildEndDateExtraFields(activity, newEndDate, ctx, violatesConstraint =
3286
3420
  );
3287
3421
  }
3288
3422
  const newDuration = calculateNewDuration(activity, newEndDate, ctx);
3289
- if (!isMilestone) {
3290
- const flippedType = typeForNewDuration(activity.durationHours, newDuration);
3291
- if (flippedType) fields.type = flippedType;
3292
- }
3423
+ const flippedType = typeForNewDuration(activity.durationHours, newDuration);
3424
+ if (flippedType) fields.type = flippedType;
3293
3425
  Object.assign(fields, setDuration(newDuration));
3294
3426
  fields.endDate = newEndDate;
3295
3427
  return fields;
@@ -3706,6 +3838,49 @@ function setLinkFieldDynamic(state, linkId, field, value) {
3706
3838
  state.setLinkFieldDynamic(linkId, field, value);
3707
3839
  }
3708
3840
 
3841
+ // src/internal/state/clone-core-activity.ts
3842
+ function cloneCoreActivity(activity) {
3843
+ return {
3844
+ ...activity,
3845
+ startDate: new Date(activity.startDate),
3846
+ endDate: new Date(activity.endDate),
3847
+ constraintDate: cloneNullableDate(activity.constraintDate),
3848
+ dateOrigin: cloneNullableDate(activity.dateOrigin),
3849
+ newActivityIds: [...activity.newActivityIds],
3850
+ pendingRequestIds: [...activity.pendingRequestIds],
3851
+ responsableIds: [...activity.responsableIds],
3852
+ tagIds: [...activity.tagIds],
3853
+ baselinePoints: activity.baselinePoints.map((point) => ({
3854
+ ...point,
3855
+ startDate: cloneNullableDate(point.startDate),
3856
+ endDate: cloneNullableDate(point.endDate)
3857
+ })),
3858
+ baselineSnapshot: cloneBaselineSnapshot(activity.baselineSnapshot),
3859
+ criticalPath: cloneCriticalPath(activity.criticalPath)
3860
+ };
3861
+ }
3862
+ function cloneNullableDate(value) {
3863
+ return value === null ? null : new Date(value);
3864
+ }
3865
+ function cloneBaselineSnapshot(value) {
3866
+ if (value === null) return null;
3867
+ return {
3868
+ ...value,
3869
+ startDate: cloneNullableDate(value.startDate),
3870
+ endDate: cloneNullableDate(value.endDate)
3871
+ };
3872
+ }
3873
+ function cloneCriticalPath(value) {
3874
+ if (value === null) return null;
3875
+ return {
3876
+ ...value,
3877
+ earlyStart: cloneNullableDate(value.earlyStart),
3878
+ earlyFinish: cloneNullableDate(value.earlyFinish),
3879
+ lateStart: cloneNullableDate(value.lateStart),
3880
+ lateFinish: cloneNullableDate(value.lateFinish)
3881
+ };
3882
+ }
3883
+
3709
3884
  // src/dispatch/shared/snapshots.ts
3710
3885
  function collectTouchedIds(primary, changes) {
3711
3886
  const ids = /* @__PURE__ */ new Set([String(primary)]);
@@ -3723,13 +3898,7 @@ function snapshotActivities(adapter, ids) {
3723
3898
  return out;
3724
3899
  }
3725
3900
  function structuredCloneActivity(activity) {
3726
- return {
3727
- ...activity,
3728
- startDate: new Date(activity.startDate),
3729
- endDate: new Date(activity.endDate),
3730
- constraintDate: activity.constraintDate ? new Date(activity.constraintDate) : null,
3731
- dateOrigin: activity.dateOrigin ? new Date(activity.dateOrigin) : null
3732
- };
3901
+ return cloneCoreActivity(activity);
3733
3902
  }
3734
3903
  function applyFieldChanges(adapter, activityId, changes) {
3735
3904
  applyCanonicalPatch(adapter, activityId, changes.patch);
@@ -3782,12 +3951,33 @@ function diffActivity(before, after) {
3782
3951
  const afterRec = after;
3783
3952
  const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRec), ...Object.keys(afterRec)]);
3784
3953
  for (const k of keys) {
3785
- if (!shallowEqual(beforeRec[k], afterRec[k])) {
3954
+ if (Object.hasOwn(beforeRec, k) !== Object.hasOwn(afterRec, k) || !fieldValueEqual(beforeRec[k], afterRec[k])) {
3786
3955
  fields[k] = { before: beforeRec[k], after: afterRec[k] };
3787
3956
  }
3788
3957
  }
3789
3958
  return fields;
3790
3959
  }
3960
+ function fieldValueEqual(left, right) {
3961
+ if (Object.is(left, right)) return true;
3962
+ if (left instanceof Date && right instanceof Date) {
3963
+ return left.getTime() === right.getTime();
3964
+ }
3965
+ if (left == null || right == null) return false;
3966
+ if (Array.isArray(left) || Array.isArray(right)) {
3967
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
3968
+ if (left.length !== right.length) return false;
3969
+ return left.every((value, index) => fieldValueEqual(value, right[index]));
3970
+ }
3971
+ if (typeof left !== "object" || typeof right !== "object") return false;
3972
+ const leftRecord = left;
3973
+ const rightRecord = right;
3974
+ const leftKeys = Object.keys(leftRecord);
3975
+ const rightKeys = Object.keys(rightRecord);
3976
+ if (leftKeys.length !== rightKeys.length) return false;
3977
+ return leftKeys.every(
3978
+ (key) => Object.hasOwn(rightRecord, key) && fieldValueEqual(leftRecord[key], rightRecord[key])
3979
+ );
3980
+ }
3791
3981
  function collectDirtyForInlineEdit(triggerId, touchedIds, scheduledIds) {
3792
3982
  const dirty = /* @__PURE__ */ new Set();
3793
3983
  dirty.add(triggerId);
@@ -3813,21 +4003,6 @@ function foldCorrelativeShifts(adapter, shifts, excludeIds, beforeSnap, touched)
3813
4003
  beforeSnap.set(shift.activityId, beforeImage);
3814
4004
  }
3815
4005
  }
3816
- function shallowEqual(a, b) {
3817
- if (a === b) return true;
3818
- if (a instanceof Date && b instanceof Date)
3819
- return a.getTime() === b.getTime();
3820
- if (a == null || b == null) return false;
3821
- if (typeof a !== typeof b) return false;
3822
- if (Array.isArray(a) && Array.isArray(b)) {
3823
- if (a.length !== b.length) return false;
3824
- for (let i = 0; i < a.length; i++) {
3825
- if (!shallowEqual(a[i], b[i])) return false;
3826
- }
3827
- return true;
3828
- }
3829
- return false;
3830
- }
3831
4006
 
3832
4007
  // src/shared/backend-date.ts
3833
4008
  var BACKEND_DATE_FORMAT_REGEX = /^(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{2})$/;
@@ -9357,6 +9532,9 @@ function resolveDerivedPasses(action, autoSchedule, options) {
9357
9532
  // src/dispatch/shared/post-mutation.ts
9358
9533
  async function runPostMutation(deps, args) {
9359
9534
  const passes = resolvePasses(args);
9535
+ if (passes.has(DERIVED_PASS.AUTOSCHEDULE)) {
9536
+ await refreshMutatedParentBounds(deps.adapter, args);
9537
+ }
9360
9538
  const schedule = passes.has(DERIVED_PASS.AUTOSCHEDULE) ? await runAutoschedule(deps, args.autoscheduleFrom) : emptySchedule();
9361
9539
  await rollUpParentBounds(deps.adapter, args, schedule.scheduledIds, passes);
9362
9540
  await runDerivedRecomputes(deps, passes, args.now);
@@ -9365,7 +9543,11 @@ async function runPostMutation(deps, args) {
9365
9543
  function resolvePasses(args) {
9366
9544
  const requested = args.autoscheduleFrom !== null;
9367
9545
  const ran = !args.options.skipAutoSchedule && requested;
9368
- return resolveDerivedPasses(args.action, { ran }, args.options);
9546
+ const passes = new Set(
9547
+ resolveDerivedPasses(args.action, { ran }, args.options)
9548
+ );
9549
+ if (args.recomputeParentProgress) passes.add(DERIVED_PASS.PARENT_PROGRESS);
9550
+ return passes;
9369
9551
  }
9370
9552
  async function runAutoschedule(deps, autoscheduleFrom) {
9371
9553
  const options = autoscheduleFrom === "roots" ? {} : { triggerId: autoscheduleFrom };
@@ -9376,20 +9558,23 @@ async function runAutoschedule(deps, autoscheduleFrom) {
9376
9558
  }
9377
9559
  function captureScheduledRows(adapter, updatedIds) {
9378
9560
  const scheduledIds = [];
9379
- const scheduledBeforeImages = /* @__PURE__ */ new Map();
9380
9561
  for (const id of updatedIds) {
9381
9562
  const activityId = String(id);
9382
9563
  scheduledIds.push(activityId);
9383
9564
  const snap = adapter.getActivity(activityId);
9384
9565
  if (snap) {
9385
- scheduledBeforeImages.set(activityId, structuredCloneActivity(snap));
9386
9566
  adapter.noteScheduledBefore(activityId, snap);
9387
9567
  }
9388
9568
  }
9389
- return { scheduledIds, scheduledBeforeImages };
9569
+ return { scheduledIds };
9390
9570
  }
9391
9571
  function emptySchedule() {
9392
- return { scheduledIds: [], scheduledBeforeImages: /* @__PURE__ */ new Map() };
9572
+ return { scheduledIds: [] };
9573
+ }
9574
+ async function refreshMutatedParentBounds(adapter, args) {
9575
+ const mutated = args.collectDirty ? args.collectDirty([]) : collectDirtyForStructural(args.recomputeParentsFrom, []);
9576
+ if (mutated.size === 0) return;
9577
+ await updateParentBoundsFromChildren(adapter, mutated, false);
9393
9578
  }
9394
9579
  async function rollUpParentBounds(adapter, args, scheduledIds, passes) {
9395
9580
  const dirty = args.collectDirty ? args.collectDirty(scheduledIds) : collectDirtyForStructural(args.recomputeParentsFrom, scheduledIds);
@@ -9398,17 +9583,20 @@ async function rollUpParentBounds(adapter, args, scheduledIds, passes) {
9398
9583
  dirty,
9399
9584
  passes.has(DERIVED_PASS.PARENT_PROGRESS),
9400
9585
  {
9401
- recomputeRealWork: passes.has(DERIVED_PASS.REAL_WORK)
9586
+ recomputeRealWork: passes.has(DERIVED_PASS.REAL_WORK),
9587
+ progressMode: args.recomputeParentProgress ? "zero-when-no-contribution" : "freeze-when-no-contribution"
9402
9588
  }
9403
9589
  );
9404
9590
  }
9405
9591
  async function runDerivedRecomputes(deps, passes, now) {
9406
9592
  if (passes.has(DERIVED_PASS.EXPECTED_PROGRESS) && now !== null) {
9407
- runExpectedProgressBase(
9408
- deps.adapter,
9409
- now,
9410
- deps.defaultBaseCalendarId ?? null
9411
- );
9593
+ if (hasActiveBaseline(deps.adapter)) {
9594
+ runExpectedProgressBase(
9595
+ deps.adapter,
9596
+ now,
9597
+ deps.defaultBaseCalendarId ?? null
9598
+ );
9599
+ }
9412
9600
  applyExpectedProgressLive(deps.adapter, now);
9413
9601
  applyStatusPass(deps.adapter, deps.sector.statusCriteria);
9414
9602
  }
@@ -9419,6 +9607,13 @@ async function runDerivedRecomputes(deps, passes, now) {
9419
9607
  await applyCriticalPath(deps.adapter, deps.sector.hoursPerDay);
9420
9608
  }
9421
9609
  }
9610
+ function hasActiveBaseline(adapter) {
9611
+ let active = false;
9612
+ adapter.forEachActivity((activity) => {
9613
+ if (getActiveBaseline(activity)) active = true;
9614
+ });
9615
+ return active;
9616
+ }
9422
9617
 
9423
9618
  // src/dispatch/handlers/revert-no-op-constraint-edit.ts
9424
9619
  var CONSTRAINT_EDIT_COLUMNS = /* @__PURE__ */ new Set([
@@ -9429,6 +9624,9 @@ var MUST_CONSTRAINTS = /* @__PURE__ */ new Set(["mso", "mfo"]);
9429
9624
  function isConstraintEditColumn(column) {
9430
9625
  return CONSTRAINT_EDIT_COLUMNS.has(column);
9431
9626
  }
9627
+ function isExplicitConstraintDateEdit(column) {
9628
+ return column === COLUMN.CONSTRAINT_DATE;
9629
+ }
9432
9630
  function revertNoOpConstraintEdit(activityId, priorConstraintDate, adapter, calendars) {
9433
9631
  const activity = adapter.getActivity(activityId);
9434
9632
  if (!activity) return false;
@@ -9472,7 +9670,7 @@ function sameDate(first, second) {
9472
9670
 
9473
9671
  // src/dispatch/shared/constraint-settle.ts
9474
9672
  async function settleConstraintEdits(deps, { postMutationArgs, revertTargets, revert }) {
9475
- let { scheduledIds, scheduledBeforeImages } = await runPostMutation(
9673
+ let { scheduledIds } = await runPostMutation(
9476
9674
  deps,
9477
9675
  withCriticalPathSuppressed(postMutationArgs)
9478
9676
  );
@@ -9487,15 +9685,11 @@ async function settleConstraintEdits(deps, { postMutationArgs, revertTargets, re
9487
9685
  withCriticalPathSuppressed(postMutationArgs)
9488
9686
  );
9489
9687
  scheduledIds = mergeScheduledIds(scheduledIds, resettled.scheduledIds);
9490
- scheduledBeforeImages = mergeScheduledBeforeImagesFirstWins(
9491
- scheduledBeforeImages,
9492
- resettled.scheduledBeforeImages
9493
- );
9494
9688
  }
9495
9689
  if (resolveRunCriticalPath(postMutationArgs.action)) {
9496
9690
  await runPostMutation(deps, criticalPathOnlyArgs(postMutationArgs));
9497
9691
  }
9498
- return { scheduledIds, scheduledBeforeImages };
9692
+ return { scheduledIds };
9499
9693
  }
9500
9694
  function withCriticalPathSuppressed(args) {
9501
9695
  return {
@@ -9517,13 +9711,6 @@ function mergeScheduledIds(firstSettleIds, secondSettleIds) {
9517
9711
  for (const scheduledId of secondSettleIds) merged.add(scheduledId);
9518
9712
  return [...merged];
9519
9713
  }
9520
- function mergeScheduledBeforeImagesFirstWins(firstSettleImages, secondSettleImages) {
9521
- const merged = new Map(firstSettleImages);
9522
- for (const [imageId, image] of secondSettleImages) {
9523
- if (!merged.has(imageId)) merged.set(imageId, image);
9524
- }
9525
- return merged;
9526
- }
9527
9714
 
9528
9715
  // src/dispatch/sir-auto-reject.ts
9529
9716
  function datesChanged(before, after) {
@@ -9558,33 +9745,12 @@ function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
9558
9745
 
9559
9746
  // src/dispatch/shared/change-set.ts
9560
9747
  function assembleChangeSet(adapter, args) {
9561
- const allTouched = /* @__PURE__ */ new Set();
9562
- for (const id of args.touchedIds) allTouched.add(id);
9563
- for (const id of args.scheduledIds) allTouched.add(id);
9564
- if (args.includeAllIds) {
9565
- for (const id of adapter.getAllIds()) allTouched.add(id);
9566
- }
9748
+ const allTouched = collectChangeSetIds(adapter, args);
9567
9749
  const before = new Map(args.beforeSnap);
9568
9750
  const captured = adapter.peekWriteCapture();
9569
9751
  if (captured) {
9570
- for (const id of captured.dirty) {
9571
- if (allTouched.has(id)) continue;
9572
- const beforeImg = captured.before.get(id);
9573
- const live = adapter.getActivity(id);
9574
- if (!beforeImg || !live) continue;
9575
- if (Object.keys(diffActivity(beforeImg, live)).length === 0) continue;
9576
- before.set(id, beforeImg);
9577
- allTouched.add(id);
9578
- }
9579
- }
9580
- if (args.scheduledBeforeImages) {
9581
- for (const [id, img] of args.scheduledBeforeImages) {
9582
- const sid = String(id);
9583
- if (!before.has(sid)) {
9584
- before.set(sid, img);
9585
- allTouched.add(sid);
9586
- }
9587
- }
9752
+ const insertedIds = collectInsertedIds(captured);
9753
+ mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
9588
9754
  }
9589
9755
  const beforeDiffEffects = args.sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
9590
9756
  const activityChanges = buildActivityChanges(adapter, before, allTouched);
@@ -9601,6 +9767,31 @@ function assembleChangeSet(adapter, args) {
9601
9767
  ...warnings.length > 0 ? { warnings } : {}
9602
9768
  };
9603
9769
  }
9770
+ function collectChangeSetIds(adapter, args) {
9771
+ const ids = /* @__PURE__ */ new Set();
9772
+ for (const id of args.touchedIds) ids.add(id);
9773
+ for (const id of args.scheduledIds) ids.add(id);
9774
+ if (args.includeAllIds) {
9775
+ for (const id of adapter.getAllIds()) ids.add(id);
9776
+ }
9777
+ return ids;
9778
+ }
9779
+ function mergeDirtyBeforeImages(adapter, captured, insertedIds, before, touched) {
9780
+ for (const id of captured.dirty) {
9781
+ if (insertedIds.has(id) || before.has(id)) continue;
9782
+ const beforeImage = captured.before.get(id);
9783
+ const live = adapter.getActivity(id);
9784
+ if (!beforeImage || !live) continue;
9785
+ if (Object.keys(diffActivity(beforeImage, live)).length === 0) continue;
9786
+ before.set(id, beforeImage);
9787
+ touched.add(id);
9788
+ }
9789
+ }
9790
+ function collectInsertedIds(captured) {
9791
+ return new Set(
9792
+ captured.ops.filter((operation) => operation.kind === "insert").map((operation) => operation.activityId)
9793
+ );
9794
+ }
9604
9795
 
9605
9796
  // src/dispatch/conversion.ts
9606
9797
  function snapshotToLink(l) {
@@ -9783,7 +9974,8 @@ async function dispatchInlineEdit(action, options, deps) {
9783
9974
  runPostProcessorsOnAdapter(
9784
9975
  action.activityId,
9785
9976
  changes.postProcessors ?? [],
9786
- adapter
9977
+ adapter,
9978
+ beforeSnap.get(String(action.activityId)) ?? null
9787
9979
  );
9788
9980
  const linkChanges = diffIncomingLinkLagChanges(
9789
9981
  beforeLinkLags,
@@ -9808,7 +10000,7 @@ async function dispatchInlineEdit(action, options, deps) {
9808
10000
  options
9809
10001
  });
9810
10002
  const isConstraintPath = isConstraintEditColumn(action.column);
9811
- const { scheduledIds, scheduledBeforeImages } = await (isConstraintPath ? settleConstraintEdits(
10003
+ const { scheduledIds } = await (isConstraintPath ? settleConstraintEdits(
9812
10004
  {
9813
10005
  adapter,
9814
10006
  scheduler,
@@ -9817,9 +10009,7 @@ async function dispatchInlineEdit(action, options, deps) {
9817
10009
  },
9818
10010
  {
9819
10011
  postMutationArgs: buildPostMutationArgs(),
9820
- revertTargets: [
9821
- { activityId: action.activityId, priorConstraintDate }
9822
- ],
10012
+ revertTargets: isExplicitConstraintDateEdit(action.column) ? [] : [{ activityId: action.activityId, priorConstraintDate }],
9823
10013
  revert: (activityId, capturedConstraintDate) => revertNoOpConstraintEdit(
9824
10014
  activityId,
9825
10015
  capturedConstraintDate,
@@ -9851,7 +10041,6 @@ async function dispatchInlineEdit(action, options, deps) {
9851
10041
  beforeSnap,
9852
10042
  touchedIds,
9853
10043
  scheduledIds,
9854
- scheduledBeforeImages,
9855
10044
  sirDetection: "before-diff",
9856
10045
  links: linkChanges,
9857
10046
  trackingEvents,
@@ -9865,6 +10054,40 @@ function columnInvalidatesExpandedLinks(column) {
9865
10054
  }
9866
10055
 
9867
10056
  // src/dispatch/handlers/link.ts
10057
+ function collectBatchContext(operations, adapter) {
10058
+ const activityIds = /* @__PURE__ */ new Set();
10059
+ const links = /* @__PURE__ */ new Map();
10060
+ for (const operation of operations) {
10061
+ if (operation.kind === "create") {
10062
+ activityIds.add(String(operation.source));
10063
+ activityIds.add(String(operation.target));
10064
+ continue;
10065
+ }
10066
+ const link = adapter.getLink(operation.linkId);
10067
+ if (!link) continue;
10068
+ activityIds.add(String(link.source));
10069
+ activityIds.add(String(link.target));
10070
+ links.set(operation.linkId, { ...link });
10071
+ }
10072
+ return { activityIds, links };
10073
+ }
10074
+ function applyBatchOperations(operations, adapter, linkIdGen, deterministicIds) {
10075
+ return operations.map((operation, index) => {
10076
+ const deterministicId = operation.kind === "create" ? deterministicIds?.get(index) : void 0;
10077
+ const result = applyLinkOperation(operation, {
10078
+ port: adapter,
10079
+ newLinkId: deterministicId ? () => deterministicId : linkIdGen.next
10080
+ });
10081
+ return {
10082
+ op: operation,
10083
+ finalLinkId: result.applied ? result.linkId : null,
10084
+ rejected: result.applied ? null : result.rejected ?? "link_op_rejected"
10085
+ };
10086
+ });
10087
+ }
10088
+ function stringKeyedLinkSnapshots(links) {
10089
+ return new Map([...links].map(([id, link]) => [String(id), link]));
10090
+ }
9868
10091
  async function dispatchLink(action, options, deps) {
9869
10092
  if (action.kind === "link-create" || action.kind === "link-update") {
9870
10093
  const type = action.type;
@@ -9922,40 +10145,14 @@ function batchOpToLinkOperation(op, hoursPerDay) {
9922
10145
  async function dispatchLinkBatch(operations, source, options, deps, deterministicIds) {
9923
10146
  const { adapter, scheduler, sector, linkIdGen } = deps;
9924
10147
  scheduler.invalidateAllCaches();
9925
- const affectedActivityIds = /* @__PURE__ */ new Set();
9926
- const beforeLinks = /* @__PURE__ */ new Map();
9927
- for (const op of operations) {
9928
- if (op.kind === "create") {
9929
- affectedActivityIds.add(String(op.source));
9930
- affectedActivityIds.add(String(op.target));
9931
- } else {
9932
- const link = adapter.getLink(op.linkId);
9933
- if (link) {
9934
- affectedActivityIds.add(String(link.source));
9935
- affectedActivityIds.add(String(link.target));
9936
- beforeLinks.set(op.linkId, { ...link });
9937
- }
9938
- }
9939
- }
10148
+ const { activityIds: affectedActivityIds, links: beforeLinks } = collectBatchContext(operations, adapter);
9940
10149
  const beforeActivities = snapshotActivities(adapter, affectedActivityIds);
9941
- const applied = [];
9942
- for (let i = 0; i < operations.length; i++) {
9943
- const op = operations[i];
9944
- const detId = op.kind === "create" && deterministicIds ? deterministicIds.get(i) : void 0;
9945
- const result = applyLinkOperation(op, {
9946
- port: adapter,
9947
- // Si el caller pasó un id deterministic (test override, o el bridge
9948
- // pasando el id que DHTMLX iba a usar), lo respetamos. Si no, el
9949
- // generador del core arranca desde Date.now() — mismo formato que
9950
- // DHTMLX uid(). Ver `generators/link-id-generator.ts`.
9951
- newLinkId: detId ? () => detId : linkIdGen.next
9952
- });
9953
- applied.push({
9954
- op,
9955
- finalLinkId: result.applied ? result.linkId : null,
9956
- rejected: result.applied ? null : result.rejected ?? "link_op_rejected"
9957
- });
9958
- }
10150
+ const applied = applyBatchOperations(
10151
+ operations,
10152
+ adapter,
10153
+ linkIdGen,
10154
+ deterministicIds
10155
+ );
9959
10156
  if (operations.length === 1 && applied[0] && applied[0].rejected !== null && source.kind !== "inline-edit") {
9960
10157
  return { ok: false, reason: applied[0].rejected };
9961
10158
  }
@@ -9975,20 +10172,23 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
9975
10172
  options
9976
10173
  }
9977
10174
  );
9978
- for (const id of scheduledIds) {
9979
- if (!beforeActivities.has(id)) {
9980
- const a = adapter.getActivity(id);
9981
- if (a) beforeActivities.set(id, structuredCloneActivity(a));
9982
- }
9983
- }
9984
10175
  const linkChanges = buildLinkChangesForBatch(applied, beforeLinks, adapter);
9985
- const beforeLinksByStringId = /* @__PURE__ */ new Map();
9986
- for (const [linkId, snap] of beforeLinks) {
9987
- beforeLinksByStringId.set(String(linkId), snap);
9988
- }
9989
10176
  return {
9990
10177
  ok: true,
9991
- __beforeLinks: beforeLinksByStringId,
10178
+ linkVerdicts: applied.map(
10179
+ (operation, operationIndex) => operation.rejected === null ? {
10180
+ operationIndex,
10181
+ kind: operation.op.kind,
10182
+ ok: true,
10183
+ linkId: String(operation.finalLinkId)
10184
+ } : {
10185
+ operationIndex,
10186
+ kind: operation.op.kind,
10187
+ ok: false,
10188
+ reason: operation.rejected
10189
+ }
10190
+ ),
10191
+ __beforeLinks: stringKeyedLinkSnapshots(beforeLinks),
9992
10192
  changes: assembleChangeSet(adapter, {
9993
10193
  source,
9994
10194
  beforeSnap: beforeActivities,
@@ -10361,8 +10561,7 @@ var NEW_ACTIVITY_DEFAULTS = Object.freeze({
10361
10561
  expectedProgress: null,
10362
10562
  expectedProgressBaseline: null,
10363
10563
  status: null,
10364
- criticalPath: null,
10365
- promotionRestore: null
10564
+ criticalPath: null
10366
10565
  });
10367
10566
  var FIRST_ACTIVITY_TEXT = "New Master Plan";
10368
10567
  var NEW_ACTIVITY_TEXT = "New Activity";
@@ -10467,6 +10666,23 @@ function resolveStartDate(parent, fallback, override) {
10467
10666
  }
10468
10667
 
10469
10668
  // src/creation/parent-mutation-pipeline.ts
10669
+ function snapshotOf(parent) {
10670
+ const type = parent.type;
10671
+ if (type !== "task" && type !== "milestone") return null;
10672
+ const startDate = parent.startDate;
10673
+ const endDate = parent.endDate;
10674
+ if (!(startDate instanceof Date) || !(endDate instanceof Date)) return null;
10675
+ return {
10676
+ type,
10677
+ startDate: new Date(startDate),
10678
+ endDate: new Date(endDate),
10679
+ durationHours: Number(parent.durationHours ?? 0),
10680
+ expectedProgressBaseline: typeof parent.expectedProgressBaseline === "number" ? parent.expectedProgressBaseline : null,
10681
+ constraintType: parent.constraintType ?? null,
10682
+ constraintDate: parent.constraintDate instanceof Date ? new Date(parent.constraintDate) : null,
10683
+ progress: Number(parent.progress ?? 0)
10684
+ };
10685
+ }
10470
10686
  function buildParentMutations(input) {
10471
10687
  const parent = input.getParent(input.parentId);
10472
10688
  if (!parent) return null;
@@ -10485,14 +10701,18 @@ function buildParentMutations(input) {
10485
10701
  }
10486
10702
  if (wasPromotableLeaf) {
10487
10703
  fields.type = PROMOTION_TARGET_TYPE;
10488
- fields.constraintType = CONSTRAINT_TYPE2.ASAP;
10489
- fields.constraintDate = null;
10490
10704
  if ((input.promotionSource ?? "create") === "create") {
10705
+ fields.constraintType = CONSTRAINT_TYPE2.ASAP;
10706
+ fields.constraintDate = null;
10491
10707
  fields.progress = 0;
10492
10708
  fields.expectedProgressBaseline = 0;
10493
10709
  }
10494
10710
  }
10495
- return { parentId: input.parentId, fields };
10711
+ return {
10712
+ parentId: input.parentId,
10713
+ fields,
10714
+ promotionSnapshot: snapshotOf(parent)
10715
+ };
10496
10716
  }
10497
10717
  function isPromotableLeaf(parent) {
10498
10718
  return isPromotableLeafType(parent.type) && (!Array.isArray(parent.newActivityIds) || parent.newActivityIds.length === 0);
@@ -10718,6 +10938,12 @@ function applyParentMutations(action, newId, parent, adapter, preserveParentCust
10718
10938
  if (skipCustomIdMutation) continue;
10719
10939
  setActivityFieldDynamic(adapter, parentMutations.parentId, key, value);
10720
10940
  }
10941
+ if (parentMutations.promotionSnapshot) {
10942
+ adapter.setPromotionSnapshot(
10943
+ parentMutations.parentId,
10944
+ parentMutations.promotionSnapshot
10945
+ );
10946
+ }
10721
10947
  }
10722
10948
  function recomputeAndFoldCorrelatives(adapter, newId, beforeSnap, initialTouched, opts) {
10723
10949
  if (opts.skipCorrelativeRecompute) return;
@@ -10731,7 +10957,7 @@ function recomputeAndFoldCorrelatives(adapter, newId, beforeSnap, initialTouched
10731
10957
  initialTouched
10732
10958
  );
10733
10959
  }
10734
- function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPerDay, scheduledBeforeImages) {
10960
+ function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPerDay) {
10735
10961
  const { newId, newActivity, beforeSnap, initialTouched } = coreResult;
10736
10962
  const trackingEvent = {
10737
10963
  name: DISPATCH_TRACK_EVENT.ACTIVITY_CREATION,
@@ -10744,17 +10970,11 @@ function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPe
10744
10970
  if (action.eventSource !== void 0) {
10745
10971
  trackingEvent.properties.event_source = action.eventSource;
10746
10972
  }
10747
- const existingScheduledBeforeImages = scheduledBeforeImages ? new Map(
10748
- [...scheduledBeforeImages].filter(
10749
- ([scheduledId]) => String(scheduledId) !== String(newId)
10750
- )
10751
- ) : void 0;
10752
10973
  return assembleChangeSet(adapter, {
10753
10974
  source: action,
10754
10975
  beforeSnap,
10755
10976
  touchedIds: initialTouched,
10756
10977
  scheduledIds,
10757
- scheduledBeforeImages: existingScheduledBeforeImages,
10758
10978
  trackingEvents: [trackingEvent]});
10759
10979
  }
10760
10980
  async function dispatchActivityCreate(action, options, deps) {
@@ -10784,14 +11004,13 @@ async function dispatchActivityCreate(action, options, deps) {
10784
11004
  } finally {
10785
11005
  adapter.setActivityField(newId, "autoScheduling", createdAutoScheduling);
10786
11006
  }
10787
- const { scheduledIds, scheduledBeforeImages } = scheduleOutcome;
11007
+ const { scheduledIds } = scheduleOutcome;
10788
11008
  const changeset = buildCreateChangeSet(
10789
11009
  adapter,
10790
11010
  action,
10791
11011
  core,
10792
11012
  scheduledIds,
10793
- sector.hoursPerDay,
10794
- scheduledBeforeImages
11013
+ sector.hoursPerDay
10795
11014
  );
10796
11015
  return { ok: true, changes: changeset };
10797
11016
  }
@@ -10936,7 +11155,7 @@ async function dispatchActivityPaste(action, options, deps) {
10936
11155
  rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
10937
11156
  });
10938
11157
  }
10939
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11158
+ const { scheduledIds } = await runPostMutation(
10940
11159
  {
10941
11160
  adapter,
10942
11161
  scheduler,
@@ -10963,14 +11182,6 @@ async function dispatchActivityPaste(action, options, deps) {
10963
11182
  beforeSnap,
10964
11183
  touchedIds: touched,
10965
11184
  scheduledIds,
10966
- // Existing rows the autoscheduler moved need a before-image (the global
10967
- // beforeSnap used to supply it). Pass scheduledBeforeImages but filter out
10968
- // the pasted rows — their pre-schedule image would flip them to `updated`.
10969
- scheduledBeforeImages: new Map(
10970
- [...scheduledBeforeImages].filter(
10971
- ([schedId]) => !createdIdSet.has(String(schedId))
10972
- )
10973
- ),
10974
11185
  links: linkChanges,
10975
11186
  hoursPerDay: sector.hoursPerDay
10976
11187
  })
@@ -11105,10 +11316,8 @@ function collectIncidentLinkIds(activityIds, adapter) {
11105
11316
  }
11106
11317
 
11107
11318
  // src/internal/hierarchy/parent-demotion.ts
11108
- function restoredPromotionFields(parent) {
11109
- const restore = parent.promotionRestore;
11110
- if (typeof restore !== "object" || restore === null) return null;
11111
- if (restore.type !== "task" && restore.type !== "milestone") return null;
11319
+ function restoredPromotionFields(restore) {
11320
+ if (!restore) return null;
11112
11321
  return {
11113
11322
  type: restore.type,
11114
11323
  startDate: new Date(restore.startDate),
@@ -11117,16 +11326,20 @@ function restoredPromotionFields(parent) {
11117
11326
  expectedProgressBaseline: restore.expectedProgressBaseline,
11118
11327
  constraintType: restore.constraintType,
11119
11328
  constraintDate: restore.constraintDate ? new Date(restore.constraintDate) : null,
11120
- progress: restore.progress,
11121
- promotionRestore: null
11329
+ progress: restore.progress
11122
11330
  };
11123
11331
  }
11332
+ function canonicalEndDateField(parent, durationHours, computeEndDate) {
11333
+ const startDate = parent.startDate;
11334
+ if (!computeEndDate || !(startDate instanceof Date)) return {};
11335
+ return { endDate: computeEndDate(startDate, durationHours) };
11336
+ }
11124
11337
  function buildParentDemotionMutations(input) {
11125
11338
  if (input.remainingChildIds.length > 0) return null;
11126
11339
  const parent = input.parent;
11127
11340
  const canonicalDuration = input.defaultDurationHours;
11128
11341
  const hasCanonicalDefaults = Number.isFinite(canonicalDuration);
11129
- const fields = restoredPromotionFields(parent) ?? (hasCanonicalDefaults ? {
11342
+ const fields = restoredPromotionFields(input.promotionSnapshot) ?? (hasCanonicalDefaults ? {
11130
11343
  type: DEMOTION_TARGET_TYPE,
11131
11344
  durationHours: canonicalDuration,
11132
11345
  progress: 0,
@@ -11134,7 +11347,11 @@ function buildParentDemotionMutations(input) {
11134
11347
  constraintType: "asap",
11135
11348
  constraintDate: null,
11136
11349
  hasNewActivities: false,
11137
- promotionRestore: null
11350
+ ...canonicalEndDateField(
11351
+ parent,
11352
+ canonicalDuration,
11353
+ input.computeEndDate
11354
+ )
11138
11355
  } : { type: DEMOTION_TARGET_TYPE });
11139
11356
  if (input.idsRemoved && input.idsRemoved.size > 0) {
11140
11357
  const existing = Array.isArray(parent.newActivityIds) ? parent.newActivityIds : [];
@@ -11231,6 +11448,8 @@ async function dispatchActivityDelete(action, options, deps) {
11231
11448
  parent,
11232
11449
  remainingChildIds: remainingChildren.map(String),
11233
11450
  defaultDurationHours: sector.hoursPerDay,
11451
+ promotionSnapshot: adapter.getPromotionSnapshot(parentId),
11452
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({ startDate, durationHours, task: parent }),
11234
11453
  idsRemoved: toDelete
11235
11454
  });
11236
11455
  if (!demotion) {
@@ -11266,7 +11485,7 @@ async function dispatchActivityDelete(action, options, deps) {
11266
11485
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11267
11486
  beforeSnap.set(shift.activityId, beforeImage);
11268
11487
  }
11269
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11488
+ const { scheduledIds } = await runPostMutation(
11270
11489
  {
11271
11490
  adapter,
11272
11491
  scheduler,
@@ -11303,7 +11522,6 @@ async function dispatchActivityDelete(action, options, deps) {
11303
11522
  beforeSnap,
11304
11523
  touchedIds,
11305
11524
  scheduledIds,
11306
- scheduledBeforeImages,
11307
11525
  sirDetection: "after-diff",
11308
11526
  links: buildLinkDeletions(beforeLinks),
11309
11527
  trackingEvents: [trackingEvent],
@@ -11475,6 +11693,12 @@ async function dispatchActivityMove(action, options, deps) {
11475
11693
  for (const [key, value] of Object.entries(promotion.fields)) {
11476
11694
  setActivityFieldDynamic(adapter, promotion.parentId, key, value);
11477
11695
  }
11696
+ if (promotion.promotionSnapshot) {
11697
+ adapter.setPromotionSnapshot(
11698
+ promotion.parentId,
11699
+ promotion.promotionSnapshot
11700
+ );
11701
+ }
11478
11702
  if (promotion.fields.customId === null) {
11479
11703
  releaseClearedCustomId(oldNewParentCustomId, deps.customIdTracker);
11480
11704
  }
@@ -11489,6 +11713,12 @@ async function dispatchActivityMove(action, options, deps) {
11489
11713
  parent: oldParentActivity,
11490
11714
  remainingChildIds: remaining.map(String),
11491
11715
  defaultDurationHours: sector.hoursPerDay,
11716
+ promotionSnapshot: adapter.getPromotionSnapshot(oldParentKey),
11717
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({
11718
+ startDate,
11719
+ durationHours,
11720
+ task: oldParentActivity
11721
+ }),
11492
11722
  idsRemoved: /* @__PURE__ */ new Set([action.activityId])
11493
11723
  });
11494
11724
  if (demotion) {
@@ -11528,7 +11758,8 @@ async function dispatchActivityMove(action, options, deps) {
11528
11758
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11529
11759
  beforeSnap.set(shift.activityId, beforeImage);
11530
11760
  }
11531
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11761
+ const sourceParentDirtyIds = parentChanged && oldParentKey !== ROOT_PARENT_ID ? adapter.getChildren(oldParentKey).map(String) : [];
11762
+ const { scheduledIds } = await runPostMutation(
11532
11763
  {
11533
11764
  adapter,
11534
11765
  scheduler,
@@ -11538,9 +11769,14 @@ async function dispatchActivityMove(action, options, deps) {
11538
11769
  {
11539
11770
  action,
11540
11771
  autoscheduleFrom: action.activityId,
11541
- recomputeParentsFrom: [action.activityId, action.parentId],
11772
+ recomputeParentsFrom: [
11773
+ action.activityId,
11774
+ action.parentId,
11775
+ ...sourceParentDirtyIds
11776
+ ],
11542
11777
  now: deps.now,
11543
- options
11778
+ options,
11779
+ recomputeParentProgress: parentChanged
11544
11780
  }
11545
11781
  );
11546
11782
  const invalidLinkIds = parentChanged ? collectInvalidLinkIdsForCycle([action.activityId], adapter) : [];
@@ -11564,7 +11800,6 @@ async function dispatchActivityMove(action, options, deps) {
11564
11800
  beforeSnap,
11565
11801
  touchedIds: touched,
11566
11802
  scheduledIds,
11567
- scheduledBeforeImages,
11568
11803
  sirDetection: "before-diff",
11569
11804
  trackingEvents: [trackingEvent],
11570
11805
  hoursPerDay: sector.hoursPerDay
@@ -11654,6 +11889,12 @@ async function dispatchActivityIndent(action, options, deps) {
11654
11889
  for (const [key, value] of Object.entries(promotion.fields)) {
11655
11890
  setActivityFieldDynamic(adapter, promotion.parentId, key, value);
11656
11891
  }
11892
+ if (promotion.promotionSnapshot) {
11893
+ adapter.setPromotionSnapshot(
11894
+ promotion.parentId,
11895
+ promotion.promotionSnapshot
11896
+ );
11897
+ }
11657
11898
  if (!wasTrackedAsNewChild) {
11658
11899
  adapter.setActivityField(
11659
11900
  newParentId,
@@ -11699,7 +11940,7 @@ async function dispatchActivityIndent(action, options, deps) {
11699
11940
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11700
11941
  beforeSnap.set(shift.activityId, beforeImage);
11701
11942
  }
11702
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
11943
+ const { scheduledIds } = await runPostMutation(
11703
11944
  {
11704
11945
  adapter,
11705
11946
  scheduler,
@@ -11756,12 +11997,21 @@ async function dispatchActivityIndent(action, options, deps) {
11756
11997
  }
11757
11998
  return {
11758
11999
  ok: true,
12000
+ activityVerdicts: action.activityIds.map((activityId) => {
12001
+ const failure = failed.find(
12002
+ (entry) => String(entry.activityId) === String(activityId)
12003
+ );
12004
+ return failure ? {
12005
+ activityId: String(activityId),
12006
+ ok: false,
12007
+ reason: failure.reason
12008
+ } : { activityId: String(activityId), ok: true };
12009
+ }),
11759
12010
  changes: assembleChangeSet(adapter, {
11760
12011
  source: action,
11761
12012
  beforeSnap,
11762
12013
  touchedIds: touched,
11763
12014
  scheduledIds,
11764
- scheduledBeforeImages,
11765
12015
  trackingEvents: [trackingEvent],
11766
12016
  hoursPerDay: sector.hoursPerDay
11767
12017
  })
@@ -11883,6 +12133,12 @@ async function dispatchActivityOutdent(action, options, deps) {
11883
12133
  parent: parentActivity,
11884
12134
  remainingChildIds: remaining.map(String),
11885
12135
  defaultDurationHours: sector.hoursPerDay,
12136
+ promotionSnapshot: adapter.getPromotionSnapshot(oldParentKey),
12137
+ computeEndDate: (startDate, durationHours) => adapter.calculateEndDate({
12138
+ startDate,
12139
+ durationHours,
12140
+ task: parentActivity
12141
+ }),
11886
12142
  idsRemoved: new Set(
11887
12143
  planned.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
11888
12144
  )
@@ -11928,7 +12184,7 @@ async function dispatchActivityOutdent(action, options, deps) {
11928
12184
  if (shift.before !== void 0) beforeImage.correlativeId = shift.before;
11929
12185
  beforeSnap.set(shift.activityId, beforeImage);
11930
12186
  }
11931
- const { scheduledIds, scheduledBeforeImages } = await runPostMutation(
12187
+ const { scheduledIds } = await runPostMutation(
11932
12188
  {
11933
12189
  adapter,
11934
12190
  scheduler,
@@ -11961,12 +12217,21 @@ async function dispatchActivityOutdent(action, options, deps) {
11961
12217
  }
11962
12218
  return {
11963
12219
  ok: true,
12220
+ activityVerdicts: action.activityIds.map((activityId) => {
12221
+ const failure = failed.find(
12222
+ (entry) => String(entry.activityId) === String(activityId)
12223
+ );
12224
+ return failure ? {
12225
+ activityId: String(activityId),
12226
+ ok: false,
12227
+ reason: failure.reason
12228
+ } : { activityId: String(activityId), ok: true };
12229
+ }),
11964
12230
  changes: assembleChangeSet(adapter, {
11965
12231
  source: action,
11966
12232
  beforeSnap,
11967
12233
  touchedIds: touched,
11968
12234
  scheduledIds,
11969
- scheduledBeforeImages,
11970
12235
  trackingEvents: [trackingEvent],
11971
12236
  hoursPerDay: sector.hoursPerDay
11972
12237
  })
@@ -12058,11 +12323,15 @@ async function dispatchDatesBatch(action, options, deps) {
12058
12323
  mergeBeforeSnapshots(beforeSnap, editTouched, deps);
12059
12324
  for (const id of editTouched) touchedIds.add(id);
12060
12325
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12326
+ const preEditSnapshot = snapshotActivities(adapter, /* @__PURE__ */ new Set([String(edit.activityId)])).get(
12327
+ String(edit.activityId)
12328
+ ) ?? null;
12061
12329
  applyFieldChanges(adapter, edit.activityId, outcome.changes);
12062
12330
  runPostProcessorsOnAdapter(
12063
12331
  edit.activityId,
12064
12332
  outcome.changes.postProcessors ?? [],
12065
- adapter
12333
+ adapter,
12334
+ preEditSnapshot
12066
12335
  );
12067
12336
  linkChanges.push(
12068
12337
  ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
@@ -12197,6 +12466,7 @@ async function dispatchBulkEdit(action, options, deps) {
12197
12466
  const verdicts = [];
12198
12467
  const beforeSnap = /* @__PURE__ */ new Map();
12199
12468
  const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12469
+ const explicitDateEditActivities = /* @__PURE__ */ new Set();
12200
12470
  const touchedIds = /* @__PURE__ */ new Set();
12201
12471
  const trackingEvents = [];
12202
12472
  const linkChanges = [];
@@ -12215,14 +12485,21 @@ async function dispatchBulkEdit(action, options, deps) {
12215
12485
  mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
12216
12486
  for (const touchedId of editTouched) touchedIds.add(touchedId);
12217
12487
  recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12488
+ if (isExplicitConstraintDateEdit(edit.column)) {
12489
+ explicitDateEditActivities.add(String(edit.activityId));
12490
+ }
12218
12491
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12492
+ const preEditSnapshot = snapshotActivities(adapter, /* @__PURE__ */ new Set([String(edit.activityId)])).get(
12493
+ String(edit.activityId)
12494
+ ) ?? null;
12219
12495
  const customIdBeforeApply = readLiveCustomId(edit, deps);
12220
12496
  const changes = outcome.changes;
12221
12497
  applyFieldChanges(adapter, edit.activityId, changes);
12222
12498
  runPostProcessorsOnAdapter(
12223
12499
  edit.activityId,
12224
12500
  outcome.changes.postProcessors ?? [],
12225
- adapter
12501
+ adapter,
12502
+ preEditSnapshot
12226
12503
  );
12227
12504
  syncCustomIdTracker(edit, customIdBeforeApply, deps);
12228
12505
  linkChanges.push(
@@ -12244,7 +12521,7 @@ async function dispatchBulkEdit(action, options, deps) {
12244
12521
  options
12245
12522
  };
12246
12523
  const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12247
- const { scheduledIds, scheduledBeforeImages } = await (batchHasConstraintEdits ? settleConstraintEdits(
12524
+ const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
12248
12525
  {
12249
12526
  adapter,
12250
12527
  scheduler,
@@ -12253,12 +12530,12 @@ async function dispatchBulkEdit(action, options, deps) {
12253
12530
  },
12254
12531
  {
12255
12532
  postMutationArgs,
12256
- revertTargets: [...constraintPriorsByActivity].map(
12257
- ([activityId, priorConstraintDate]) => ({
12258
- activityId,
12259
- priorConstraintDate
12260
- })
12261
- ),
12533
+ revertTargets: [...constraintPriorsByActivity].filter(
12534
+ ([activityId]) => !explicitDateEditActivities.has(activityId)
12535
+ ).map(([activityId, priorConstraintDate]) => ({
12536
+ activityId,
12537
+ priorConstraintDate
12538
+ })),
12262
12539
  revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12263
12540
  activityId,
12264
12541
  priorConstraintDate,
@@ -12286,7 +12563,6 @@ async function dispatchBulkEdit(action, options, deps) {
12286
12563
  // Los dependientes que mueve el autoscheduler no están en beforeSnap;
12287
12564
  // sin su before-image el diff los marca FALSE-CREATED y el undo los
12288
12565
  // borra (fix 4bbccbc de inline-edit).
12289
- scheduledBeforeImages,
12290
12566
  sirDetection: "before-diff",
12291
12567
  links: linkChanges,
12292
12568
  trackingEvents,
@@ -15885,8 +16161,15 @@ var WriteCapture = class {
15885
16161
  /** Pre-applyResults clone of an activity the autoscheduler will move (first-wins). */
15886
16162
  noteScheduledBefore(activityId, current) {
15887
16163
  const journal = this._journal;
15888
- if (!journal || journal.scheduledBefore.has(activityId)) return;
15889
- journal.scheduledBefore.set(activityId, cloneCoreActivity(current));
16164
+ if (!journal) return;
16165
+ const beforeImage = cloneCoreActivity(current);
16166
+ if (!journal.scheduledBefore.has(activityId)) {
16167
+ journal.scheduledBefore.set(activityId, beforeImage);
16168
+ }
16169
+ if (!journal.before.has(activityId)) {
16170
+ journal.before.set(activityId, beforeImage);
16171
+ }
16172
+ journal.dirty.add(activityId);
15890
16173
  }
15891
16174
  /** Field write on a link — pre-mutation clone on first write per link id. */
15892
16175
  noteLinkField(linkId, current) {
@@ -15900,20 +16183,6 @@ var WriteCapture = class {
15900
16183
  });
15901
16184
  }
15902
16185
  };
15903
- function cloneCoreActivity(activity) {
15904
- return {
15905
- ...activity,
15906
- startDate: new Date(activity.startDate),
15907
- endDate: new Date(activity.endDate),
15908
- constraintDate: activity.constraintDate ? new Date(activity.constraintDate) : null,
15909
- dateOrigin: activity.dateOrigin ? new Date(activity.dateOrigin) : null,
15910
- newActivityIds: [...activity.newActivityIds],
15911
- pendingRequestIds: [...activity.pendingRequestIds],
15912
- responsableIds: [...activity.responsableIds],
15913
- tagIds: [...activity.tagIds],
15914
- baselinePoints: [...activity.baselinePoints]
15915
- };
15916
- }
15917
16186
  function cloneLink(link) {
15918
16187
  return { ...link };
15919
16188
  }
@@ -16167,6 +16436,13 @@ var ScheduleState = class {
16167
16436
  // whether a soft constraint repositioned the activity. Never persisted,
16168
16437
  // never in the ChangeSet — not domain data on CoreActivity.
16169
16438
  _lastStartDate = /* @__PURE__ */ new Map();
16439
+ // Transient gesture state: what a leaf looked like right before it gained its
16440
+ // first child and became a summary. Read by the demotion to give the activity
16441
+ // its own values back instead of the aggregate its children left behind. Not
16442
+ // persisted and not in the ChangeSet: it only makes sense inside the session
16443
+ // that performed the promotion. Kept (not consumed) so undo/redo of a
16444
+ // demotion restores identically every time.
16445
+ _promotionSnapshot = /* @__PURE__ */ new Map();
16170
16446
  _links = /* @__PURE__ */ new Map();
16171
16447
  _outgoing = /* @__PURE__ */ new Map();
16172
16448
  _incoming = /* @__PURE__ */ new Map();
@@ -16182,6 +16458,7 @@ var ScheduleState = class {
16182
16458
  _viewState = new ViewStateStore();
16183
16459
  _viewStateBefore = null;
16184
16460
  _lastStartDateBefore = null;
16461
+ _promotionSnapshotBefore = null;
16185
16462
  // Calendar arithmetic + in-dispatch memoization. Assigned in the
16186
16463
  // constructor once the calendar reader is built. See `CalendarCalculator`.
16187
16464
  _calendar;
@@ -16266,6 +16543,19 @@ var ScheduleState = class {
16266
16543
  this._captureLastStartDateOnce();
16267
16544
  this._lastStartDate.set(String(activityId), startDate);
16268
16545
  }
16546
+ getPromotionSnapshot(activityId) {
16547
+ return this._promotionSnapshot.get(String(activityId)) ?? null;
16548
+ }
16549
+ setPromotionSnapshot(activityId, snapshot) {
16550
+ this._capturePromotionSnapshotOnce();
16551
+ this._promotionSnapshot.set(String(activityId), snapshot);
16552
+ }
16553
+ _capturePromotionSnapshotOnce() {
16554
+ if (this._writeCapture.peek() === null) return;
16555
+ if (this._promotionSnapshotBefore === null) {
16556
+ this._promotionSnapshotBefore = new Map(this._promotionSnapshot);
16557
+ }
16558
+ }
16269
16559
  _captureLastStartDateOnce() {
16270
16560
  if (this._writeCapture.peek() === null) return;
16271
16561
  if (this._lastStartDateBefore === null) {
@@ -16382,6 +16672,7 @@ var ScheduleState = class {
16382
16672
  this._writeCapture.begin();
16383
16673
  this._viewStateBefore = null;
16384
16674
  this._lastStartDateBefore = null;
16675
+ this._promotionSnapshotBefore = null;
16385
16676
  }
16386
16677
  /** The live journal, or null when no capture is active. */
16387
16678
  peekWriteCapture() {
@@ -16392,6 +16683,7 @@ var ScheduleState = class {
16392
16683
  this._writeCapture.end();
16393
16684
  this._viewStateBefore = null;
16394
16685
  this._lastStartDateBefore = null;
16686
+ this._promotionSnapshotBefore = null;
16395
16687
  }
16396
16688
  // -- Replay-specific mutators --------------------------------------------
16397
16689
  setActivityField(activityId, field, value) {
@@ -16543,6 +16835,9 @@ var ScheduleState = class {
16543
16835
  if (this._viewStateBefore !== null) {
16544
16836
  this._viewState.restore(this._viewStateBefore);
16545
16837
  }
16838
+ if (this._promotionSnapshotBefore !== null) {
16839
+ this._promotionSnapshot = this._promotionSnapshotBefore;
16840
+ }
16546
16841
  if (this._lastStartDateBefore !== null) {
16547
16842
  this._lastStartDate = this._lastStartDateBefore;
16548
16843
  }
@@ -16675,6 +16970,13 @@ function readSelectedActivityIds(state) {
16675
16970
  return state.checkedIds().map(String);
16676
16971
  }
16677
16972
  function readHasChild(state, parentId) {
16973
+ if (parentId === ROOT_PARENT_ID) {
16974
+ let hasRoot = false;
16975
+ state.forEachActivity((activity) => {
16976
+ hasRoot ||= isRootParent(activity.parentId);
16977
+ });
16978
+ return hasRoot;
16979
+ }
16678
16980
  return state.getChildren(parentId).length > 0;
16679
16981
  }
16680
16982
  function readLink(state, id) {
@@ -16817,6 +17119,7 @@ function buildInverseChangeSet(state, entry, side) {
16817
17119
  activities.push({
16818
17120
  id: activityId,
16819
17121
  kind: "updated",
17122
+ fields: historyFields(change.fields, side),
16820
17123
  after: snapshot ? structuredCloneActivity(snapshot) : null
16821
17124
  });
16822
17125
  } else if (change.kind === "deleted") {
@@ -16850,6 +17153,7 @@ function buildInverseChangeSet(state, entry, side) {
16850
17153
  links.push({
16851
17154
  id: linkId,
16852
17155
  kind: "updated",
17156
+ fields: historyFields(change.fields, side),
16853
17157
  after: lk ? snapshotToLink(lk) : null
16854
17158
  });
16855
17159
  } else if (change.kind === "deleted") {
@@ -16889,6 +17193,21 @@ function buildInverseChangeSet(state, entry, side) {
16889
17193
  viewState
16890
17194
  };
16891
17195
  }
17196
+ function historyFields(fields, side) {
17197
+ if (fields == null) return void 0;
17198
+ return Object.fromEntries(
17199
+ Object.entries(fields).map(([field, values]) => [
17200
+ field,
17201
+ side === "before" ? {
17202
+ before: cloneDomainValue(values.after),
17203
+ after: cloneDomainValue(values.before)
17204
+ } : {
17205
+ before: cloneDomainValue(values.before),
17206
+ after: cloneDomainValue(values.after)
17207
+ }
17208
+ ])
17209
+ );
17210
+ }
16892
17211
  function invertViewStateChange(change) {
16893
17212
  return {
16894
17213
  activityId: change.activityId,
@@ -17427,8 +17746,7 @@ function parseActivity(raw, context) {
17427
17746
  expectedProgress: null,
17428
17747
  expectedProgressBaseline: null,
17429
17748
  status: null,
17430
- criticalPath: null,
17431
- promotionRestore: null
17749
+ criticalPath: null
17432
17750
  };
17433
17751
  }
17434
17752
  function forceRootProjectToRoot(activities) {
@@ -18864,9 +19182,11 @@ var ScheduleCore = class {
18864
19182
  * ineligible to commit.
18865
19183
  */
18866
19184
  recomputeCriticalPath() {
18867
- return this._enqueue(async () => ({
19185
+ const operation = this._enqueue(async () => ({
18868
19186
  job: this._startCriticalPathForCurrentRevision()
18869
19187
  })).then(({ job }) => job);
19188
+ this._criticalPathReady = operation;
19189
+ return operation;
18870
19190
  }
18871
19191
  isCriticalPathSettled() {
18872
19192
  return this._criticalPathRevision === this._scheduleRevision && this._activeCriticalPath === null;
@@ -19000,11 +19320,11 @@ var ScheduleCore = class {
19000
19320
  if (this.coreRuntime.clock) {
19001
19321
  const now = endOfLocalDay(this.coreRuntime.clock());
19002
19322
  applyExpectedProgressLive(state, now);
19003
- let hasActiveBaseline = false;
19323
+ let hasActiveBaseline2 = false;
19004
19324
  state.forEachActivity((activity) => {
19005
- hasActiveBaseline ||= getActiveBaseline(activity) !== null;
19325
+ hasActiveBaseline2 ||= getActiveBaseline(activity) !== null;
19006
19326
  });
19007
- if (hasActiveBaseline) {
19327
+ if (hasActiveBaseline2) {
19008
19328
  runExpectedProgressBase(
19009
19329
  state,
19010
19330
  now,