@outbuild-company/schedule-core 1.6.0 → 1.6.2

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
@@ -210,19 +210,19 @@ function dispatchSelectionReplace(action, deps) {
210
210
  }
211
211
 
212
212
  // src/dispatch/shared/apply-visible-set.ts
213
- function applyVisibleSet(adapter, visibleIds) {
213
+ function applyVisibleSet(adapter, visibleIds, activityIds = adapter.getAllIds()) {
214
214
  const viewState = [];
215
- adapter.forEachActivity((_snapshot, activityId) => {
215
+ for (const activityId of activityIds) {
216
216
  const id = String(activityId);
217
217
  const willBeVisible = visibleIds.has(id);
218
218
  const before = adapter.isVisible(id);
219
- if (before === willBeVisible) return;
219
+ if (before === willBeVisible) continue;
220
220
  adapter.setVisible(id, willBeVisible);
221
221
  viewState.push({
222
222
  activityId: id,
223
223
  visible: { before, after: willBeVisible }
224
224
  });
225
- });
225
+ }
226
226
  return viewState;
227
227
  }
228
228
 
@@ -496,6 +496,12 @@ function overlapsRange(activity, range) {
496
496
  const windowStartsInside = windowStart >= taskStart && windowStart <= taskEnd;
497
497
  return fullyInside || startsInside || windowStartsInside;
498
498
  }
499
+ function matchesFilter(activity, filter, context) {
500
+ const passesCriteria = matchesCriteria(activity, filter, context);
501
+ const range = filter.dateRange;
502
+ const passesRange = range === void 0 || overlapsRange(activity, range);
503
+ return passesCriteria && passesRange;
504
+ }
499
505
  function addAncestors(matchedId, parentOf, visibleIds) {
500
506
  let ancestorId = parentOf(matchedId);
501
507
  while (ancestorId !== null && !visibleIds.has(String(ancestorId))) {
@@ -505,13 +511,10 @@ function addAncestors(matchedId, parentOf, visibleIds) {
505
511
  }
506
512
  function evaluateVisibleIds(input) {
507
513
  const { activities, parentOf, filter, context } = input;
508
- const range = filter.dateRange;
509
514
  const visibleIds = /* @__PURE__ */ new Set();
510
515
  const matchedIds = [];
511
516
  for (const activity of activities) {
512
- const passesCriteria = matchesCriteria(activity, filter, context);
513
- const passesRange = range === void 0 || overlapsRange(activity, range);
514
- if (passesCriteria && passesRange) {
517
+ if (matchesFilter(activity, filter, context)) {
515
518
  visibleIds.add(String(activity.id));
516
519
  matchedIds.push(activity.id);
517
520
  }
@@ -521,6 +524,114 @@ function evaluateVisibleIds(input) {
521
524
  }
522
525
  return visibleIds;
523
526
  }
527
+ function seedFilterProjection(input) {
528
+ const { activities, parentOf, filter, context } = input;
529
+ const matchedIds = /* @__PURE__ */ new Set();
530
+ const pinnedVisibleIds = /* @__PURE__ */ new Set();
531
+ const visibleReferenceCounts = /* @__PURE__ */ new Map();
532
+ const parentIds = /* @__PURE__ */ new Map();
533
+ const remainingChildren = /* @__PURE__ */ new Map();
534
+ for (const activity of activities) {
535
+ const activityId = String(activity.id);
536
+ const parentId = parentOf(activityId);
537
+ parentIds.set(activityId, parentId);
538
+ remainingChildren.set(activityId, remainingChildren.get(activityId) ?? 0);
539
+ if (parentId !== null) {
540
+ remainingChildren.set(
541
+ parentId,
542
+ (remainingChildren.get(parentId) ?? 0) + 1
543
+ );
544
+ }
545
+ const isMatch = matchesFilter(activity, filter, context);
546
+ const isPinned = input.pinnedVisibleIds?.has(activityId) === true;
547
+ visibleReferenceCounts.set(
548
+ activityId,
549
+ (isMatch ? 1 : 0) + (isPinned ? 1 : 0)
550
+ );
551
+ if (isMatch) matchedIds.add(activityId);
552
+ if (isPinned) pinnedVisibleIds.add(activityId);
553
+ }
554
+ return {
555
+ matchedIds,
556
+ pinnedVisibleIds,
557
+ visibleReferenceCounts,
558
+ parentIds,
559
+ remainingChildren
560
+ };
561
+ }
562
+ function propagateVisibleReferenceCounts(seed2) {
563
+ const { visibleReferenceCounts, parentIds, remainingChildren } = seed2;
564
+ const pendingIds = [];
565
+ for (const activityId of parentIds.keys()) {
566
+ if (remainingChildren.get(activityId) === 0) pendingIds.push(activityId);
567
+ }
568
+ while (pendingIds.length > 0) {
569
+ const activityId = pendingIds.pop();
570
+ if (activityId === void 0) break;
571
+ const parentId = parentIds.get(activityId);
572
+ if (parentId === null || parentId === void 0) continue;
573
+ const referenceCount = visibleReferenceCounts.get(activityId) ?? 0;
574
+ visibleReferenceCounts.set(
575
+ parentId,
576
+ (visibleReferenceCounts.get(parentId) ?? 0) + referenceCount
577
+ );
578
+ const remaining = (remainingChildren.get(parentId) ?? 1) - 1;
579
+ remainingChildren.set(parentId, remaining);
580
+ if (remaining === 0) pendingIds.push(parentId);
581
+ }
582
+ }
583
+ function collectVisibleIds(visibleReferenceCounts) {
584
+ const visibleIds = /* @__PURE__ */ new Set();
585
+ for (const [activityId, referenceCount] of visibleReferenceCounts) {
586
+ if (referenceCount > 0) visibleIds.add(activityId);
587
+ }
588
+ return visibleIds;
589
+ }
590
+ function buildFilterProjection(input) {
591
+ const seed2 = seedFilterProjection(input);
592
+ propagateVisibleReferenceCounts(seed2);
593
+ const visibleIds = collectVisibleIds(seed2.visibleReferenceCounts);
594
+ return {
595
+ filter: input.filter,
596
+ context: input.context,
597
+ matchedIds: seed2.matchedIds,
598
+ pinnedVisibleIds: seed2.pinnedVisibleIds,
599
+ visibleIds,
600
+ visibleReferenceCounts: seed2.visibleReferenceCounts
601
+ };
602
+ }
603
+ function updateFilterProjection(projection, activities, parentOf) {
604
+ const changedVisibilityIds = /* @__PURE__ */ new Set();
605
+ for (const activity of activities) {
606
+ const activityId = String(activity.id);
607
+ changedVisibilityIds.add(activityId);
608
+ const wasMatch = projection.matchedIds.has(activityId);
609
+ const isMatch = matchesFilter(
610
+ activity,
611
+ projection.filter,
612
+ projection.context
613
+ );
614
+ if (wasMatch === isMatch) continue;
615
+ if (isMatch) projection.matchedIds.add(activityId);
616
+ else projection.matchedIds.delete(activityId);
617
+ const referenceDelta = isMatch ? 1 : -1;
618
+ let currentId = activityId;
619
+ while (currentId !== null) {
620
+ const beforeCount = projection.visibleReferenceCounts.get(currentId) ?? 0;
621
+ const afterCount = beforeCount + referenceDelta;
622
+ projection.visibleReferenceCounts.set(currentId, afterCount);
623
+ const wasVisible = beforeCount > 0;
624
+ const isVisible = afterCount > 0;
625
+ if (wasVisible !== isVisible) {
626
+ changedVisibilityIds.add(currentId);
627
+ if (isVisible) projection.visibleIds.add(currentId);
628
+ else projection.visibleIds.delete(currentId);
629
+ }
630
+ currentId = parentOf(currentId);
631
+ }
632
+ }
633
+ return changedVisibilityIds;
634
+ }
524
635
 
525
636
  // src/internal/filter/context.ts
526
637
  var COLLATION_LOCALE = "en";
@@ -581,17 +692,26 @@ function isValidDateValue(value) {
581
692
  function isEmptyFilter(filter) {
582
693
  return filter.criteria.length === 0 && filter.dateRange === void 0;
583
694
  }
584
- function resolveVisibleIds(adapter, filter, hoursPerDay) {
695
+ function resolveFilter(adapter, filter, hoursPerDay) {
585
696
  const activities = adapter.getAllActivities();
586
697
  if (isEmptyFilter(filter)) {
587
- return new Set(activities.map((activity) => String(activity.id)));
698
+ return {
699
+ visibleIds: evaluateVisibleIds({
700
+ activities,
701
+ parentOf: (activityId) => adapter.getParentId(activityId),
702
+ filter,
703
+ context: buildFilterContext(hoursPerDay)
704
+ }),
705
+ projection: null
706
+ };
588
707
  }
589
- return evaluateVisibleIds({
708
+ const projection = buildFilterProjection({
590
709
  activities,
591
710
  parentOf: (activityId) => adapter.getParentId(activityId),
592
711
  filter,
593
712
  context: buildFilterContext(hoursPerDay)
594
713
  });
714
+ return { visibleIds: projection.visibleIds, projection };
595
715
  }
596
716
  function dispatchFilterSet(action, deps) {
597
717
  const { adapter, hoursPerDay } = deps;
@@ -605,7 +725,12 @@ function dispatchFilterSet(action, deps) {
605
725
  dateRange: action.dateRange
606
726
  };
607
727
  adapter.setActiveFilter(isEmptyFilter(filter) ? null : filter);
608
- const visibleIds = resolveVisibleIds(adapter, filter, hoursPerDay);
728
+ const { visibleIds, projection } = resolveFilter(
729
+ adapter,
730
+ filter,
731
+ hoursPerDay
732
+ );
733
+ adapter.setFilterProjection(projection);
609
734
  const viewState = applyVisibleSet(adapter, visibleIds);
610
735
  const changes = {
611
736
  source: action,
@@ -676,11 +801,7 @@ function visualIndexInParent(taskId, adapter) {
676
801
  return siblings.findIndex((id) => String(id) === String(taskId));
677
802
  }
678
803
  function collectChildrenSnapshots(parentId, adapter) {
679
- const key = String(parentId);
680
- if (key === "0") {
681
- return adapter.getAllActivities().filter((a) => a.parentId === null);
682
- }
683
- const childIds = adapter.getChildren(parentId);
804
+ const childIds = String(parentId) === ROOT_PARENT_ID ? adapter.getRootIds?.() ?? adapter.getChildren(ROOT_PARENT_ID) : adapter.getChildren(parentId);
684
805
  const out = [];
685
806
  for (const id of childIds) {
686
807
  const snap = adapter.getActivity(id);
@@ -724,6 +845,14 @@ function collectBranchOrder(adapter) {
724
845
  visit(ROOT_PARENT_ID);
725
846
  return branches;
726
847
  }
848
+ function collectBranchOrderForParents(adapter, parentIds) {
849
+ const branches = [];
850
+ for (const parentId of parentIds) {
851
+ const childIds = getChildrenInVisualOrder(parentId, adapter);
852
+ if (childIds.length > 1) branches.push({ parentId, childIds });
853
+ }
854
+ return branches;
855
+ }
727
856
 
728
857
  // src/dispatch/order.ts
729
858
  function validateRule(rule) {
@@ -2275,6 +2404,34 @@ function wouldIntroduceCycle(existingLinks, proposedLink) {
2275
2404
  }
2276
2405
  return hasPath(adjacency, tgt, src);
2277
2406
  }
2407
+ function hasDirectedCycle(links) {
2408
+ const adjacency = /* @__PURE__ */ new Map();
2409
+ const indegrees = /* @__PURE__ */ new Map();
2410
+ for (const link of links) {
2411
+ const source = String(link.source);
2412
+ const target = String(link.target);
2413
+ const neighbors = adjacency.get(source) ?? [];
2414
+ neighbors.push(target);
2415
+ adjacency.set(source, neighbors);
2416
+ indegrees.set(source, indegrees.get(source) ?? 0);
2417
+ indegrees.set(target, (indegrees.get(target) ?? 0) + 1);
2418
+ }
2419
+ const ready = new Queue();
2420
+ for (const [activityId, indegree] of indegrees) {
2421
+ if (indegree === 0) ready.enqueue(activityId);
2422
+ }
2423
+ let visited = 0;
2424
+ while (!ready.isEmpty()) {
2425
+ const current = ready.dequeue();
2426
+ visited += 1;
2427
+ for (const neighbor of adjacency.get(current) ?? []) {
2428
+ const indegree = indegrees.get(neighbor) - 1;
2429
+ indegrees.set(neighbor, indegree);
2430
+ if (indegree === 0) ready.enqueue(neighbor);
2431
+ }
2432
+ }
2433
+ return visited !== indegrees.size;
2434
+ }
2278
2435
  function hasPath(adjacency, from, to) {
2279
2436
  if (from === to) return true;
2280
2437
  const visited = /* @__PURE__ */ new Set([from]);
@@ -2304,6 +2461,70 @@ function applyLinkOperation(op, deps) {
2304
2461
  return applyDelete(op, deps);
2305
2462
  }
2306
2463
  }
2464
+ function applyLinkCreates(links, deps) {
2465
+ if (links.length === 0) return { applied: true };
2466
+ const existingLinks = deps.port.getAllLinks();
2467
+ const duplicateKeys = /* @__PURE__ */ new Map();
2468
+ const edges = [];
2469
+ for (const link of existingLinks) {
2470
+ const key = linkKey(link);
2471
+ if (!duplicateKeys.has(key)) duplicateKeys.set(key, link.id);
2472
+ edges.push({ source: link.source, target: link.target });
2473
+ }
2474
+ const existingGraphHasCycle = hasDirectedCycle(edges);
2475
+ let rejected = null;
2476
+ for (const { operation, linkId } of links) {
2477
+ if (String(operation.source) === String(operation.target)) {
2478
+ rejected = { applied: false, rejected: "self-link" };
2479
+ break;
2480
+ }
2481
+ if (!deps.port.getActivity(operation.source) || !deps.port.getActivity(operation.target)) {
2482
+ rejected = { applied: false, rejected: "activity-missing" };
2483
+ break;
2484
+ }
2485
+ const key = linkKey(operation);
2486
+ const duplicateId = duplicateKeys.get(key);
2487
+ if (duplicateId !== void 0) {
2488
+ rejected = {
2489
+ applied: false,
2490
+ rejected: "duplicate",
2491
+ linkId: duplicateId
2492
+ };
2493
+ break;
2494
+ }
2495
+ duplicateKeys.set(key, linkId);
2496
+ if (existingGraphHasCycle && // ponytail: preserve legacy sequential semantics; optimize with dynamic
2497
+ // SCC reachability only if corrupt cyclic schedules become measurable.
2498
+ wouldIntroduceCycle(edges, {
2499
+ source: operation.source,
2500
+ target: operation.target
2501
+ })) {
2502
+ return { applied: false, rejected: "cycle" };
2503
+ }
2504
+ edges.push({ source: operation.source, target: operation.target });
2505
+ }
2506
+ if (!existingGraphHasCycle && hasDirectedCycle(edges)) {
2507
+ return { applied: false, rejected: "cycle" };
2508
+ }
2509
+ if (rejected) return rejected;
2510
+ for (const { operation, linkId } of links) {
2511
+ const preserved = deps.preserved?.get(linkId);
2512
+ deps.port.addLink({
2513
+ id: linkId,
2514
+ source: operation.source,
2515
+ target: operation.target,
2516
+ type: operation.type,
2517
+ lag: operation.lag,
2518
+ ...preserved?.ganttId === void 0 ? {} : { ganttId: preserved.ganttId },
2519
+ ...preserved?.sectorId === void 0 ? {} : { sectorId: preserved.sectorId },
2520
+ ...preserved?.proplannerId === void 0 ? {} : { proplannerId: preserved.proplannerId }
2521
+ });
2522
+ }
2523
+ return { applied: true };
2524
+ }
2525
+ function linkKey(link) {
2526
+ return JSON.stringify([String(link.source), String(link.target), link.type]);
2527
+ }
2307
2528
  function applyCreate(op, deps) {
2308
2529
  const { port } = deps;
2309
2530
  if (String(op.source) === String(op.target)) {
@@ -2427,14 +2648,14 @@ function diffLinks(input) {
2427
2648
  unresolved.push(spec);
2428
2649
  continue;
2429
2650
  }
2430
- const key = linkKey(source, spec.type);
2651
+ const key = linkKey2(source, spec.type);
2431
2652
  if (desired.has(key)) continue;
2432
2653
  desired.set(key, { source, type: spec.type, lag: spec.lag });
2433
2654
  }
2434
2655
  const currentByKey = /* @__PURE__ */ new Map();
2435
2656
  for (const link of currentLinks) {
2436
2657
  if (String(link.target) !== String(targetActivityId)) continue;
2437
- currentByKey.set(linkKey(link.source, link.type), link);
2658
+ currentByKey.set(linkKey2(link.source, link.type), link);
2438
2659
  }
2439
2660
  const operations = [];
2440
2661
  for (const [key, link] of currentByKey) {
@@ -2461,7 +2682,7 @@ function diffLinks(input) {
2461
2682
  }
2462
2683
  return { operations, unresolved };
2463
2684
  }
2464
- function linkKey(source, type) {
2685
+ function linkKey2(source, type) {
2465
2686
  return `${String(source)}::${type}`;
2466
2687
  }
2467
2688
  function diffOutgoingLinks(input) {
@@ -2474,14 +2695,14 @@ function diffOutgoingLinks(input) {
2474
2695
  unresolved.push(spec);
2475
2696
  continue;
2476
2697
  }
2477
- const key = linkKey(target, spec.type);
2698
+ const key = linkKey2(target, spec.type);
2478
2699
  if (desired.has(key)) continue;
2479
2700
  desired.set(key, { target, type: spec.type, lag: spec.lag });
2480
2701
  }
2481
2702
  const currentByKey = /* @__PURE__ */ new Map();
2482
2703
  for (const link of currentLinks) {
2483
2704
  if (String(link.source) !== String(sourceActivityId)) continue;
2484
- currentByKey.set(linkKey(link.target, link.type), link);
2705
+ currentByKey.set(linkKey2(link.target, link.type), link);
2485
2706
  }
2486
2707
  const operations = [];
2487
2708
  for (const [key, link] of currentByKey) {
@@ -5548,7 +5769,7 @@ var CalculateBackwardParentsLinks = class {
5548
5769
  return false;
5549
5770
  }
5550
5771
  return allLinksCalculations.reduce(
5551
- (min, activity) => activity.lf < min.lf ? activity : min
5772
+ (min2, activity) => activity.lf < min2.lf ? activity : min2
5552
5773
  );
5553
5774
  }
5554
5775
  calculateSfRestrictionDates() {
@@ -7721,13 +7942,13 @@ var LinksConstraintCalculator2 = class extends CalculationsGenericMethods_defaul
7721
7942
  getMinDateFromLinks(allLinksCalculations) {
7722
7943
  try {
7723
7944
  if (this.constraint === CONSTRAINT_TYPES.ALAP || this.constraint === CONSTRAINT_TYPES.ASAP) {
7724
- const min = allLinksCalculations.reduce(
7725
- (min2, activity) => activity.lf < min2.lf ? activity : min2
7945
+ const min2 = allLinksCalculations.reduce(
7946
+ (min3, activity) => activity.lf < min3.lf ? activity : min3
7726
7947
  );
7727
- return min;
7948
+ return min2;
7728
7949
  }
7729
7950
  return allLinksCalculations.reduce(
7730
- (min, activity) => activity.ls < min.ls ? activity : min
7951
+ (min2, activity) => activity.ls < min2.ls ? activity : min2
7731
7952
  );
7732
7953
  } catch (error) {
7733
7954
  throw new Error(
@@ -7737,7 +7958,7 @@ var LinksConstraintCalculator2 = class extends CalculationsGenericMethods_defaul
7737
7958
  }
7738
7959
  getMinLfFromLinks(allLinksCalculations) {
7739
7960
  return allLinksCalculations.reduce(
7740
- (min, activity) => activity.lf < min.lf ? activity : min
7961
+ (min2, activity) => activity.lf < min2.lf ? activity : min2
7741
7962
  );
7742
7963
  }
7743
7964
  getMinDateBasedOnConstraint(minCalculatedDateFromLinks, constraintType = this.constraint) {
@@ -8195,7 +8416,7 @@ var BackwardPath = class extends generic_calculations_default {
8195
8416
  return false;
8196
8417
  }
8197
8418
  return allLinksCalculations.reduce(
8198
- (min, activity) => activity.lf < min.lf ? activity : min
8419
+ (min2, activity) => activity.lf < min2.lf ? activity : min2
8199
8420
  );
8200
8421
  }
8201
8422
  };
@@ -8794,7 +9015,7 @@ var CriticalPath = class {
8794
9015
  activitiesCount: this.forwardPathCalculations?.size || 0
8795
9016
  }
8796
9017
  });
8797
- return false;
9018
+ throw error;
8798
9019
  }
8799
9020
  }
8800
9021
  async calculateFloatsAsync(forward, backward, isCurrent) {
@@ -8815,7 +9036,7 @@ var CriticalPath = class {
8815
9036
  activitiesCount: this.forwardPathCalculations?.size || 0
8816
9037
  }
8817
9038
  });
8818
- return { totalFloatMap: /* @__PURE__ */ new Map(), freeFloatMap: /* @__PURE__ */ new Map() };
9039
+ throw error;
8819
9040
  }
8820
9041
  }
8821
9042
  };
@@ -8840,16 +9061,41 @@ function constraintDateForCriticalPath(activity, state) {
8840
9061
  return typedDayIsWorking ? getEffectiveConstraintDate(activity, state) : raw;
8841
9062
  }
8842
9063
  function toLegacyCalendar(api) {
9064
+ const endDateByKey = /* @__PURE__ */ new Map();
9065
+ const durationByKey = /* @__PURE__ */ new Map();
9066
+ const closestWorkTimeByKey = /* @__PURE__ */ new Map();
8843
9067
  return {
8844
- calculateEndDate: ({ start_date, duration }) => api.calculateEndDate(start_date, duration),
8845
- calculateDuration: ({ start_date, end_date }) => api.calculateDuration(start_date, end_date),
9068
+ calculateEndDate: ({ start_date, duration }) => {
9069
+ const key = `${start_date.getTime()}|${duration}`;
9070
+ const cached = endDateByKey.get(key);
9071
+ if (cached !== void 0) return new Date(cached);
9072
+ const value = api.calculateEndDate(start_date, duration);
9073
+ endDateByKey.set(key, value.getTime());
9074
+ return value;
9075
+ },
9076
+ calculateDuration: ({ start_date, end_date }) => {
9077
+ const key = `${start_date.getTime()}|${end_date.getTime()}`;
9078
+ const cached = durationByKey.get(key);
9079
+ if (cached !== void 0) return cached;
9080
+ const value = api.calculateDuration(start_date, end_date);
9081
+ durationByKey.set(key, value);
9082
+ return value;
9083
+ },
8846
9084
  getWorkHours: (date2) => api.getWorkHours(date2),
8847
9085
  $gantt: {
8848
- getClosestWorkTime: ({ date: date2, dir, unit }) => api.getClosestWorkTime({
8849
- date: date2,
8850
- direction: dir,
8851
- unit: unit ?? "hour"
8852
- })
9086
+ getClosestWorkTime: ({ date: date2, dir, unit }) => {
9087
+ const resolvedUnit = unit ?? "hour";
9088
+ const key = `${date2.getTime()}|${dir}|${resolvedUnit}`;
9089
+ const cached = closestWorkTimeByKey.get(key);
9090
+ if (cached !== void 0) return new Date(cached);
9091
+ const value = api.getClosestWorkTime({
9092
+ date: date2,
9093
+ direction: dir,
9094
+ unit: resolvedUnit
9095
+ });
9096
+ closestWorkTimeByKey.set(key, value.getTime());
9097
+ return value;
9098
+ }
8853
9099
  }
8854
9100
  };
8855
9101
  }
@@ -8929,19 +9175,19 @@ function orderByTree(leveled) {
8929
9175
  return ordered;
8930
9176
  }
8931
9177
  function projectBounds(state) {
8932
- let min = null;
8933
- let max = null;
9178
+ let min2 = null;
9179
+ let max2 = null;
8934
9180
  state.forEachActivity((activity) => {
8935
9181
  if (activity.startDate) {
8936
9182
  const start = activity.startDate.getTime();
8937
- min = min == null ? start : Math.min(min, start);
9183
+ min2 = min2 == null ? start : Math.min(min2, start);
8938
9184
  }
8939
9185
  if (activity.endDate) {
8940
9186
  const end = activity.endDate.getTime();
8941
- max = max == null ? end : Math.max(max, end);
9187
+ max2 = max2 == null ? end : Math.max(max2, end);
8942
9188
  }
8943
9189
  });
8944
- return { start_date: new Date(min ?? 0), end_date: new Date(max ?? 0) };
9190
+ return { start_date: new Date(min2 ?? 0), end_date: new Date(max2 ?? 0) };
8945
9191
  }
8946
9192
  async function createGanttAdapter(state, hoursPerDay) {
8947
9193
  const calendarCache = /* @__PURE__ */ new Map();
@@ -9008,13 +9254,27 @@ function fieldsForActivity(activityId, progress, maps) {
9008
9254
  freeSlackHours: maps.freeFloat.get(activityId)?.activityFreeFloat ?? null
9009
9255
  };
9010
9256
  }
9257
+ var datesEqual = (left, right) => left === right || left !== null && right !== null && left.getTime() === right.getTime();
9258
+ function setCriticalPathFieldsIfChanged(state, activityId, fields) {
9259
+ const activity = state.getActivity(activityId);
9260
+ const current = activity?.criticalPath;
9261
+ const next = fields.criticalPath;
9262
+ if (activity && current && datesEqual(current.earlyStart, next.earlyStart) && datesEqual(current.earlyFinish, next.earlyFinish) && datesEqual(current.lateStart, next.lateStart) && datesEqual(current.lateFinish, next.lateFinish) && Object.is(current.totalSlackHours, next.totalSlackHours) && Object.is(activity.isCritical, fields.isCritical) && Object.is(activity.freeSlackHours, fields.freeSlackHours)) {
9263
+ return;
9264
+ }
9265
+ state.setActivityFields(activityId, fields);
9266
+ }
9011
9267
  async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => true) {
9012
9268
  if (!isCurrent()) return null;
9013
9269
  const gantt = await createGanttAdapter(state, hoursPerDay);
9014
9270
  if (!isCurrent()) return null;
9015
9271
  const criticalPath = new legacy_default(gantt);
9016
9272
  const calculated = await criticalPath.calculate(gantt, isCurrent);
9017
- if (!calculated || !isCurrent()) return null;
9273
+ if (!calculated) {
9274
+ if (!isCurrent()) return null;
9275
+ throw new Error("critical path calculation aborted");
9276
+ }
9277
+ if (!isCurrent()) return null;
9018
9278
  const maps = {
9019
9279
  forward: criticalPath.forwardPathCalculations,
9020
9280
  backward: criticalPath.backwardPathCalculations,
@@ -9049,7 +9309,7 @@ async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9049
9309
  if (!fieldsByActivity || !isCurrent()) return false;
9050
9310
  for (const [activityId, fields] of fieldsByActivity) {
9051
9311
  if (!isCurrent()) return false;
9052
- state.setActivityFields(activityId, fields);
9312
+ setCriticalPathFieldsIfChanged(state, activityId, fields);
9053
9313
  }
9054
9314
  return true;
9055
9315
  }
@@ -9526,6 +9786,40 @@ function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
9526
9786
  if (!finalLinkId) continue;
9527
9787
  const after = adapter.getLink(finalLinkId);
9528
9788
  if (!after) continue;
9789
+ const before = beforeLinks.get(finalLinkId);
9790
+ if (before) {
9791
+ const fields2 = {};
9792
+ if (String(before.source) !== String(after.source)) {
9793
+ fields2.source = {
9794
+ before: String(before.source),
9795
+ after: String(after.source)
9796
+ };
9797
+ }
9798
+ if (String(before.target) !== String(after.target)) {
9799
+ fields2.target = {
9800
+ before: String(before.target),
9801
+ after: String(after.target)
9802
+ };
9803
+ }
9804
+ if (before.type !== after.type) {
9805
+ fields2.type = { before: before.type, after: after.type };
9806
+ }
9807
+ if (Number(before.lag) !== Number(after.lag)) {
9808
+ fields2.lag = {
9809
+ before: Number(before.lag),
9810
+ after: Number(after.lag)
9811
+ };
9812
+ }
9813
+ if (Object.keys(fields2).length > 0) {
9814
+ out.push({
9815
+ id: String(finalLinkId),
9816
+ kind: "updated",
9817
+ fields: fields2,
9818
+ after: snapshotToLink(after)
9819
+ });
9820
+ }
9821
+ continue;
9822
+ }
9529
9823
  out.push({
9530
9824
  id: String(finalLinkId),
9531
9825
  kind: "created",
@@ -9540,6 +9834,7 @@ function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
9540
9834
  continue;
9541
9835
  }
9542
9836
  if (op.kind === "delete") {
9837
+ if (adapter.getLink(op.linkId)) continue;
9543
9838
  const beforeLink2 = beforeLinks.get(op.linkId);
9544
9839
  out.push({
9545
9840
  id: String(op.linkId),
@@ -10376,8 +10671,8 @@ function computeInsertionCorrelativeId(adapter, parentId, afterSiblingId, before
10376
10671
  }
10377
10672
  }
10378
10673
  if (siblings.length === 0) return isRoot ? 0 : 0;
10379
- const max = Math.max(...siblings.map(readCorrelativeId));
10380
- return max + 1;
10674
+ const max2 = Math.max(...siblings.map(readCorrelativeId));
10675
+ return max2 + 1;
10381
10676
  }
10382
10677
  var NOT_SET_SORT_VALUE2 = Number.MAX_SAFE_INTEGER;
10383
10678
  var TEMP_CID_SENTINEL = Number.MAX_SAFE_INTEGER - 1;
@@ -10387,12 +10682,7 @@ function readCorrelativeId(activity) {
10387
10682
  return Number.isFinite(n) ? n : NOT_SET_SORT_VALUE2;
10388
10683
  }
10389
10684
  function collectSiblings(adapter, parentKey) {
10390
- const siblings = [];
10391
- adapter.forEachActivity((a) => {
10392
- const ap = normalizeParentKey(a.parentId);
10393
- if (ap === parentKey) siblings.push(a);
10394
- });
10395
- return siblings;
10685
+ return collectChildrenSnapshots(parentKey, adapter);
10396
10686
  }
10397
10687
  function findFirstSelectedSibling(taskId, selected, adapter) {
10398
10688
  let firstSelected = taskId;
@@ -10442,7 +10732,7 @@ function computeFractionalCidAtIndex(adapter, parentKey, targetIndex, excludeId)
10442
10732
  // src/dispatch/handlers/activity-create.ts
10443
10733
  async function createActivityCore(action, deps, opts = {}) {
10444
10734
  const { adapter, scheduler, sector, calendars, activityIdGen, uidGen } = deps;
10445
- scheduler.invalidateAllCaches();
10735
+ if (!opts.skipCacheInvalidation) scheduler.invalidateAllCaches();
10446
10736
  const target = validateCreateTarget(action, adapter);
10447
10737
  if (!target.ok) return target;
10448
10738
  const { parent, anchorSiblingId, isRoot } = target;
@@ -10471,7 +10761,7 @@ async function createActivityCore(action, deps, opts = {}) {
10471
10761
  newActivity.progress
10472
10762
  );
10473
10763
  assignCustomId(newActivity, action, deps, anchorSiblingId, parent, opts);
10474
- const insertionCorrelativeId = computeInsertionCorrelativeId(
10764
+ const insertionCorrelativeId = opts.insertionCorrelativeId ?? computeInsertionCorrelativeId(
10475
10765
  adapter,
10476
10766
  isRoot ? ROOT_PARENT_ID : action.parentId,
10477
10767
  action.afterSiblingId,
@@ -10481,12 +10771,18 @@ async function createActivityCore(action, deps, opts = {}) {
10481
10771
  const initialTouched = /* @__PURE__ */ new Set([newId]);
10482
10772
  if (parent) initialTouched.add(parent.id);
10483
10773
  const parentWasLeaf = parent !== null && adapter.getChildren(parent.id).length === 0;
10774
+ const parentCustomId = parentWasLeaf ? parent?.customId : null;
10484
10775
  const beforeSnap = opts.skipBeforeSnap ? /* @__PURE__ */ new Map() : await snapshotActivities(adapter, initialTouched);
10485
10776
  adapter.addActivity(newActivity);
10486
10777
  applyParentMutations(action, newId, parent, adapter, parentWasLeaf);
10487
- recomputeHhCascadeForActivity(newId, adapter);
10488
- recomputeUsedCostCascadeForActivity(newId, adapter);
10489
- recomputeCostCascadeForActivity(newId, adapter);
10778
+ if (parentCustomId) {
10779
+ deps.customIdTracker.releaseCustomId(parentCustomId);
10780
+ }
10781
+ if (!opts.skipRollupCascades) {
10782
+ recomputeHhCascadeForActivity(newId, adapter);
10783
+ recomputeUsedCostCascadeForActivity(newId, adapter);
10784
+ recomputeCostCascadeForActivity(newId, adapter);
10785
+ }
10490
10786
  recomputeAndFoldCorrelatives(
10491
10787
  adapter,
10492
10788
  newId,
@@ -10658,330 +10954,96 @@ function findDefaultCalendarId(calendars) {
10658
10954
  return "global";
10659
10955
  }
10660
10956
 
10661
- // src/dispatch/handlers/activity-paste.ts
10662
- function resolvePasteRootDestination(destination, adapter) {
10663
- if ("afterSiblingId" in destination) {
10664
- const sibling = adapter.getActivity(destination.afterSiblingId);
10665
- if (!sibling) {
10666
- return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
10957
+ // src/propagations/upward/recompute-rollup-cascades.ts
10958
+ function recomputeRollupCascadesForParent(parentId, adapter) {
10959
+ recomputeHhCascadeForParent(parentId, adapter);
10960
+ recomputeUsedCostCascadeForParent(parentId, adapter);
10961
+ recomputeCostCascadeForParent(parentId, adapter);
10962
+ }
10963
+
10964
+ // src/propagations/shared/traversal-helpers.ts
10965
+ function collectDescendantIds(rootIds, adapter) {
10966
+ const out = /* @__PURE__ */ new Set();
10967
+ const stack = [];
10968
+ for (const id of rootIds) {
10969
+ const key = String(id);
10970
+ if (!out.has(key)) {
10971
+ out.add(key);
10972
+ stack.push(key);
10667
10973
  }
10668
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
10669
- return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
10670
10974
  }
10671
- const parentId = destination.parentId;
10672
- const siblings = collectSiblings(adapter, parentId).sort(
10673
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
10674
- );
10675
- const before = siblings[destination.index]?.id;
10676
- return { ok: true, parentId, firstBefore: before };
10975
+ while (stack.length > 0) {
10976
+ const current = stack.pop();
10977
+ const children = adapter.getChildren(current);
10978
+ for (const childId of children) {
10979
+ const key = String(childId);
10980
+ if (!out.has(key)) {
10981
+ out.add(key);
10982
+ stack.push(key);
10983
+ }
10984
+ }
10985
+ }
10986
+ return out;
10677
10987
  }
10678
- function applyPasteDomainSemantics(overrides) {
10679
- const { customId: _customId, ...content } = overrides;
10988
+ function collectIncidentLinkIds(activityIds, adapter) {
10989
+ const out = /* @__PURE__ */ new Set();
10990
+ for (const activityId of activityIds) {
10991
+ for (const linkId of adapter.getOutgoingLinkIds(activityId)) {
10992
+ out.add(String(linkId));
10993
+ }
10994
+ for (const linkId of adapter.getIncomingLinkIds(activityId)) {
10995
+ out.add(String(linkId));
10996
+ }
10997
+ }
10998
+ return [...out];
10999
+ }
11000
+
11001
+ // src/internal/hierarchy/parent-demotion.ts
11002
+ function restoredPromotionFields(restore) {
11003
+ if (!restore) return null;
10680
11004
  return {
10681
- ...content,
10682
- progress: 0,
10683
- ponderator: 0,
10684
- usedCost: 0,
10685
- workHours: 0,
10686
- isLookahead: false
11005
+ type: restore.type,
11006
+ startDate: new Date(restore.startDate),
11007
+ endDate: new Date(restore.endDate),
11008
+ durationHours: restore.durationHours,
11009
+ expectedProgressBaseline: restore.expectedProgressBaseline,
11010
+ constraintType: restore.constraintType,
11011
+ constraintDate: restore.constraintDate ? new Date(restore.constraintDate) : null,
11012
+ progress: restore.progress
10687
11013
  };
10688
11014
  }
10689
- async function dispatchActivityPaste(action, options, deps) {
10690
- const { adapter, scheduler, sector } = deps;
10691
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
10692
- if (!rootDest.ok) return rootDest;
10693
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
10694
- const touched = /* @__PURE__ */ new Set();
10695
- const originalToNew = /* @__PURE__ */ new Map();
10696
- const createdIds = [];
10697
- let prevRootId;
10698
- for (const input of action.activities) {
10699
- const mappedParent = originalToNew.get(input.originalParentId);
10700
- let parentId;
10701
- let afterSiblingId;
10702
- let beforeSiblingId;
10703
- if (mappedParent !== void 0) {
10704
- parentId = mappedParent;
10705
- } else {
10706
- parentId = rootDest.parentId;
10707
- if (prevRootId === void 0) {
10708
- afterSiblingId = rootDest.firstAfter;
10709
- beforeSiblingId = rootDest.firstBefore;
10710
- } else {
10711
- afterSiblingId = prevRootId;
10712
- }
10713
- }
10714
- const core = await createActivityCore(
10715
- {
10716
- parentId,
10717
- afterSiblingId,
10718
- beforeSiblingId,
10719
- ...input.activityId ? { activityId: input.activityId } : {},
10720
- overrides: applyPasteDomainSemantics(input.overrides),
10721
- eventSource: action.eventSource
10722
- },
10723
- deps,
10724
- {
10725
- skipCorrelativeRecompute: true,
10726
- skipBeforeSnap: true,
10727
- customIdReferenceId: action.referenceActivityId
10728
- }
10729
- );
10730
- if (!core.ok) return core;
10731
- if (input.baselineSnapshot !== void 0) {
10732
- const snapshot = input.baselineSnapshot;
10733
- adapter.setActivityField(
10734
- core.newId,
10735
- "baselineSnapshot",
10736
- snapshot ? {
10737
- ...snapshot,
10738
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
10739
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
10740
- } : null
10741
- );
10742
- }
10743
- originalToNew.set(input.originalId, core.newId);
10744
- createdIds.push(core.newId);
10745
- if (mappedParent === void 0) prevRootId = core.newId;
10746
- }
10747
- const correlativeShifts = recomputeCorrelativeIds(adapter);
10748
- const createdIdSet = new Set(createdIds.map(String));
10749
- foldCorrelativeShifts(
10750
- adapter,
10751
- correlativeShifts,
10752
- createdIdSet,
10753
- beforeSnap,
10754
- touched
10755
- );
10756
- for (const createdId of createdIds) touched.add(String(createdId));
10757
- const appliedLinks = [];
10758
- for (const link of action.links) {
10759
- const newSource = originalToNew.get(link.source);
10760
- const newTarget = originalToNew.get(link.target);
10761
- if (newSource === void 0 || newTarget === void 0) continue;
10762
- const op = {
10763
- kind: "create",
10764
- source: newSource,
10765
- target: newTarget,
10766
- type: link.type,
10767
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
10768
- };
10769
- const res = applyLinkOperation(op, {
10770
- port: adapter,
10771
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
10772
- });
10773
- appliedLinks.push({
10774
- op,
10775
- finalLinkId: res.applied ? res.linkId : null,
10776
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
10777
- });
10778
- }
10779
- const { scheduledIds } = await runPostMutation(
10780
- {
10781
- adapter,
10782
- scheduler,
10783
- sector: deps.sector,
10784
- defaultBaseCalendarId: deps.defaultBaseCalendarId
10785
- },
10786
- {
10787
- action,
10788
- autoscheduleFrom: createdIds[0] ?? null,
10789
- recomputeParentsFrom: createdIds,
10790
- now: deps.now,
10791
- options
10792
- }
10793
- );
10794
- const linkChanges = buildLinkChangesForBatch(
10795
- appliedLinks,
10796
- /* @__PURE__ */ new Map(),
10797
- adapter
10798
- );
10799
- return {
10800
- ok: true,
10801
- changes: await assembleChangeSet(adapter, {
10802
- source: action,
10803
- beforeSnap,
10804
- touchedIds: touched,
10805
- scheduledIds,
10806
- links: linkChanges,
10807
- hoursPerDay: sector.hoursPerDay
10808
- })
10809
- };
10810
- }
10811
-
10812
- // src/dispatch/handlers/sir-sync.ts
10813
- function dispatchSirSync(action, deps) {
10814
- const { adapter } = deps;
10815
- const id = action.activityId;
10816
- const snapshot = adapter.getActivity(id);
10817
- if (!snapshot) {
10818
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
10819
- }
10820
- const before = snapshot.pendingRequestIds ?? [];
10821
- const after = action.pendingRequests.map((r) => r.id);
10822
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
10823
- const change = {
10824
- id,
10825
- kind: "updated",
10826
- fields: { pendingRequestIds: { before, after } },
10827
- after: { id, pendingRequestIds: after }
10828
- };
10829
- return {
10830
- ok: true,
10831
- changes: {
10832
- source: action,
10833
- activities: [change],
10834
- links: [],
10835
- calendars: [],
10836
- trackingEvents: []
10837
- }
10838
- };
10839
- }
10840
-
10841
- // src/dispatch/handlers/activity-lookahead-sync.ts
10842
- function dispatchActivityLookaheadSync(action, deps) {
10843
- const ids = [...new Set(action.activityIds.map(String))];
10844
- const snapshots = ids.map((id) => deps.adapter.getActivity(id));
10845
- if (snapshots.some((snapshot) => !snapshot)) {
10846
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
10847
- }
10848
- const activities = [];
10849
- for (const [index, id] of ids.entries()) {
10850
- const snapshot = snapshots[index];
10851
- if (!snapshot) continue;
10852
- const fields = {};
10853
- if (snapshot.isLookahead !== action.isLookahead) {
10854
- fields.isLookahead = {
10855
- before: snapshot.isLookahead,
10856
- after: action.isLookahead
10857
- };
10858
- deps.adapter.setActivityField(id, "isLookahead", action.isLookahead);
10859
- }
10860
- if (snapshot.hasLookaheadTasks !== action.hasLookaheadTasks) {
10861
- fields.hasLookaheadTasks = {
10862
- before: snapshot.hasLookaheadTasks,
10863
- after: action.hasLookaheadTasks
10864
- };
10865
- deps.adapter.setActivityField(
10866
- id,
10867
- "hasLookaheadTasks",
10868
- action.hasLookaheadTasks
10869
- );
10870
- }
10871
- if (Object.keys(fields).length === 0) continue;
10872
- activities.push({
10873
- id,
10874
- kind: "updated",
10875
- fields,
10876
- after: {
10877
- ...snapshot,
10878
- isLookahead: action.isLookahead,
10879
- hasLookaheadTasks: action.hasLookaheadTasks
10880
- }
10881
- });
10882
- }
10883
- return {
10884
- ok: true,
10885
- changes: {
10886
- source: action,
10887
- activities,
10888
- links: [],
10889
- calendars: [],
10890
- trackingEvents: []
10891
- }
10892
- };
10893
- }
10894
-
10895
- // src/propagations/upward/recompute-rollup-cascades.ts
10896
- function recomputeRollupCascadesForParent(parentId, adapter) {
10897
- recomputeHhCascadeForParent(parentId, adapter);
10898
- recomputeUsedCostCascadeForParent(parentId, adapter);
10899
- recomputeCostCascadeForParent(parentId, adapter);
10900
- }
10901
-
10902
- // src/propagations/shared/traversal-helpers.ts
10903
- function collectDescendantIds(rootIds, adapter) {
10904
- const out = /* @__PURE__ */ new Set();
10905
- const stack = [];
10906
- for (const id of rootIds) {
10907
- const key = String(id);
10908
- if (!out.has(key)) {
10909
- out.add(key);
10910
- stack.push(key);
10911
- }
10912
- }
10913
- while (stack.length > 0) {
10914
- const current = stack.pop();
10915
- const children = adapter.getChildren(current);
10916
- for (const childId of children) {
10917
- const key = String(childId);
10918
- if (!out.has(key)) {
10919
- out.add(key);
10920
- stack.push(key);
10921
- }
10922
- }
10923
- }
10924
- return out;
10925
- }
10926
- function collectIncidentLinkIds(activityIds, adapter) {
10927
- const out = /* @__PURE__ */ new Set();
10928
- for (const activityId of activityIds) {
10929
- for (const linkId of adapter.getOutgoingLinkIds(activityId)) {
10930
- out.add(String(linkId));
10931
- }
10932
- for (const linkId of adapter.getIncomingLinkIds(activityId)) {
10933
- out.add(String(linkId));
10934
- }
10935
- }
10936
- return [...out];
10937
- }
10938
-
10939
- // src/internal/hierarchy/parent-demotion.ts
10940
- function restoredPromotionFields(restore) {
10941
- if (!restore) return null;
10942
- return {
10943
- type: restore.type,
10944
- startDate: new Date(restore.startDate),
10945
- endDate: new Date(restore.endDate),
10946
- durationHours: restore.durationHours,
10947
- expectedProgressBaseline: restore.expectedProgressBaseline,
10948
- constraintType: restore.constraintType,
10949
- constraintDate: restore.constraintDate ? new Date(restore.constraintDate) : null,
10950
- progress: restore.progress
10951
- };
10952
- }
10953
- function canonicalEndDateField(parent, durationHours, computeEndDate) {
10954
- const startDate = parent.startDate;
10955
- if (!computeEndDate || !(startDate instanceof Date)) return {};
10956
- return { endDate: computeEndDate(startDate, durationHours) };
10957
- }
10958
- function buildParentDemotionMutations(input) {
10959
- if (input.remainingChildIds.length > 0) return null;
10960
- const parent = input.parent;
10961
- const canonicalDuration = input.defaultDurationHours;
10962
- const hasCanonicalDefaults = Number.isFinite(canonicalDuration);
10963
- const fields = restoredPromotionFields(input.promotionSnapshot) ?? (hasCanonicalDefaults ? {
10964
- type: DEMOTION_TARGET_TYPE,
10965
- durationHours: canonicalDuration,
10966
- progress: 0,
10967
- autoScheduling: true,
10968
- constraintType: "asap",
10969
- constraintDate: null,
10970
- hasNewActivities: false,
10971
- ...canonicalEndDateField(
10972
- parent,
10973
- canonicalDuration,
10974
- input.computeEndDate
10975
- )
10976
- } : { type: DEMOTION_TARGET_TYPE });
10977
- if (input.idsRemoved && input.idsRemoved.size > 0) {
10978
- const existing = Array.isArray(parent.newActivityIds) ? parent.newActivityIds : [];
10979
- if (existing.length > 0) {
10980
- const filtered = existing.filter(
10981
- (id) => !input.idsRemoved.has(id) && !input.idsRemoved.has(String(id))
10982
- );
10983
- if (filtered.length !== existing.length) {
10984
- fields.newActivityIds = filtered;
11015
+ function canonicalEndDateField(parent, durationHours, computeEndDate) {
11016
+ const startDate = parent.startDate;
11017
+ if (!computeEndDate || !(startDate instanceof Date)) return {};
11018
+ return { endDate: computeEndDate(startDate, durationHours) };
11019
+ }
11020
+ function buildParentDemotionMutations(input) {
11021
+ if (input.remainingChildIds.length > 0) return null;
11022
+ const parent = input.parent;
11023
+ const canonicalDuration = input.defaultDurationHours;
11024
+ const hasCanonicalDefaults = Number.isFinite(canonicalDuration);
11025
+ const fields = restoredPromotionFields(input.promotionSnapshot) ?? (hasCanonicalDefaults ? {
11026
+ type: DEMOTION_TARGET_TYPE,
11027
+ durationHours: canonicalDuration,
11028
+ progress: 0,
11029
+ autoScheduling: true,
11030
+ constraintType: "asap",
11031
+ constraintDate: null,
11032
+ hasNewActivities: false,
11033
+ ...canonicalEndDateField(
11034
+ parent,
11035
+ canonicalDuration,
11036
+ input.computeEndDate
11037
+ )
11038
+ } : { type: DEMOTION_TARGET_TYPE });
11039
+ if (input.idsRemoved && input.idsRemoved.size > 0) {
11040
+ const existing = Array.isArray(parent.newActivityIds) ? parent.newActivityIds : [];
11041
+ if (existing.length > 0) {
11042
+ const filtered = existing.filter(
11043
+ (id) => !input.idsRemoved.has(id) && !input.idsRemoved.has(String(id))
11044
+ );
11045
+ if (filtered.length !== existing.length) {
11046
+ fields.newActivityIds = filtered;
10985
11047
  }
10986
11048
  }
10987
11049
  }
@@ -11190,7 +11252,647 @@ function buildLinkDeletions(beforeLinks) {
11190
11252
  after: null
11191
11253
  });
11192
11254
  }
11193
- return out;
11255
+ return out;
11256
+ }
11257
+
11258
+ // src/dispatch/handlers/activity-batch.ts
11259
+ async function dispatchActivityBatch(action, options, deps) {
11260
+ const validationError = validateBatch(action, deps);
11261
+ if (validationError) return { ok: false, reason: validationError };
11262
+ const plan = buildBatchPlan(action, deps);
11263
+ if (!plan.ok) return plan;
11264
+ const existingActivityIds = action.mode === "replace" ? new Set(deps.adapter.getAllIds().map((id) => String(id))) : /* @__PURE__ */ new Set();
11265
+ const beforeSnap = action.mode === "replace" ? await snapshotActivities(deps.adapter, existingActivityIds) : /* @__PURE__ */ new Map();
11266
+ const beforeLinks = action.mode === "replace" ? snapshotAllLinks(deps) : /* @__PURE__ */ new Map();
11267
+ const viewState = action.mode === "replace" ? captureDeletedViewState(deps.adapter, existingActivityIds) : [];
11268
+ deps.scheduler.invalidateAllCaches();
11269
+ const appliedLinks = [];
11270
+ if (action.mode === "replace") {
11271
+ for (const linkId of beforeLinks.keys()) {
11272
+ const deleted = applyLinkOperation(
11273
+ { kind: "delete", linkId },
11274
+ { port: deps.adapter }
11275
+ );
11276
+ if (!deleted.applied) {
11277
+ return {
11278
+ ok: false,
11279
+ reason: deleted.rejected ?? REJECTION_REASON.INVALID
11280
+ };
11281
+ }
11282
+ appliedLinks.push({
11283
+ op: { kind: "delete", linkId },
11284
+ finalLinkId: linkId,
11285
+ rejected: null
11286
+ });
11287
+ }
11288
+ for (const activityId of existingActivityIds) {
11289
+ const customId = deps.adapter.getActivity(activityId)?.customId;
11290
+ if (typeof customId === "string" && customId.length > 0) {
11291
+ deps.customIdTracker.releaseCustomId(customId);
11292
+ }
11293
+ deps.adapter.removeActivity(activityId);
11294
+ }
11295
+ }
11296
+ const created = await createPlannedActivities(action, plan.plan.items, deps);
11297
+ if (!created.ok) return created;
11298
+ const correlativeShifts = isSimpleRootAppend(action) ? [] : recomputeCorrelativeIds(deps.adapter);
11299
+ const createdIdSet = new Set(
11300
+ plan.plan.items.map((item) => String(item.activityId))
11301
+ );
11302
+ const existingCorrelativeShifts = correlativeShifts.filter(
11303
+ (shift) => !createdIdSet.has(shift.activityId)
11304
+ );
11305
+ const touchedIds = action.mode === "replace" ? new Set(existingActivityIds) : /* @__PURE__ */ new Set();
11306
+ foldCorrelativeShifts(
11307
+ deps.adapter,
11308
+ correlativeShifts,
11309
+ createdIdSet,
11310
+ beforeSnap,
11311
+ touchedIds
11312
+ );
11313
+ for (const item of plan.plan.items) {
11314
+ touchedIds.add(String(item.activityId));
11315
+ }
11316
+ const linkCreates = plan.plan.links.map((link) => ({
11317
+ operation: {
11318
+ kind: "create",
11319
+ source: link.source,
11320
+ target: link.target,
11321
+ type: link.type,
11322
+ lag: options.inputUnit === "hours" ? link.lag : lagDaysToHours(link.lag, deps.sector.hoursPerDay)
11323
+ },
11324
+ linkId: link.linkId
11325
+ }));
11326
+ const createdLinks = applyLinkCreates(linkCreates, {
11327
+ port: deps.adapter,
11328
+ preserved: beforeLinks
11329
+ });
11330
+ if (!createdLinks.applied) {
11331
+ return {
11332
+ ok: false,
11333
+ reason: createdLinks.rejected ?? REJECTION_REASON.INVALID
11334
+ };
11335
+ }
11336
+ for (const { operation, linkId } of linkCreates) {
11337
+ appliedLinks.push({
11338
+ op: operation,
11339
+ finalLinkId: linkId,
11340
+ rejected: null
11341
+ });
11342
+ }
11343
+ const createdIds = plan.plan.items.map((item) => item.activityId);
11344
+ const { scheduledIds } = await runPostMutation(
11345
+ {
11346
+ adapter: deps.adapter,
11347
+ scheduler: deps.scheduler,
11348
+ sector: deps.sector,
11349
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
11350
+ },
11351
+ {
11352
+ action,
11353
+ autoscheduleFrom: action.items.length > 0 || action.links.length > 0 ? "roots" : null,
11354
+ recomputeParentsFrom: createdIds,
11355
+ now: deps.now,
11356
+ options
11357
+ }
11358
+ );
11359
+ const changes = await assembleChangeSet(deps.adapter, {
11360
+ source: action,
11361
+ beforeSnap,
11362
+ touchedIds,
11363
+ scheduledIds,
11364
+ links: buildLinkChangesForBatch(appliedLinks, beforeLinks, deps.adapter),
11365
+ hoursPerDay: deps.sector.hoursPerDay,
11366
+ correlativeShifts: existingCorrelativeShifts,
11367
+ ...action.mode === "replace" ? { sirDetection: "after-diff" } : {}
11368
+ });
11369
+ return {
11370
+ ok: true,
11371
+ changes: viewState.length > 0 ? { ...changes, viewState } : changes,
11372
+ ...action.mode === "replace" ? {
11373
+ __beforeSnap: beforeSnap,
11374
+ __beforeLinks: stringKeyedLinks(beforeLinks)
11375
+ } : {}
11376
+ };
11377
+ }
11378
+ function validateBatch(action, deps) {
11379
+ if (action.mode !== "append" && action.mode !== "replace") {
11380
+ return REJECTION_REASON.INVALID;
11381
+ }
11382
+ const batchIds = /* @__PURE__ */ new Set();
11383
+ const explicitActivityIds = /* @__PURE__ */ new Set();
11384
+ for (const item of action.items) {
11385
+ if (item.batchId.length === 0 || batchIds.has(item.batchId)) {
11386
+ return REJECTION_REASON.INVALID;
11387
+ }
11388
+ batchIds.add(item.batchId);
11389
+ if (item.index !== void 0 && (!Number.isInteger(item.index) || item.index < 0)) {
11390
+ return REJECTION_REASON.INVALID;
11391
+ }
11392
+ if (item.activityId !== void 0) {
11393
+ const activityId = String(item.activityId);
11394
+ if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId) || action.mode === "append" && deps.adapter.getActivity(activityId) !== null) {
11395
+ return REJECTION_REASON.INVALID;
11396
+ }
11397
+ explicitActivityIds.add(activityId);
11398
+ }
11399
+ }
11400
+ for (const item of action.items) {
11401
+ if (!validReference(item.parent, true, action.mode, batchIds, deps)) {
11402
+ return REJECTION_REASON.PARENT_NOT_FOUND;
11403
+ }
11404
+ }
11405
+ if (hasParentCycle(action.items)) return REJECTION_REASON.INVALID;
11406
+ const explicitLinkIds = /* @__PURE__ */ new Set();
11407
+ for (const link of action.links) {
11408
+ if (!Object.values(LINK_TYPE).includes(link.type) || !Number.isFinite(link.lag)) {
11409
+ return !Number.isFinite(link.lag) ? REJECTION_REASON.INVALID_LINK_LAG : REJECTION_REASON.INVALID_LINK_TYPE;
11410
+ }
11411
+ if (!validReference(link.source, false, action.mode, batchIds, deps) || !validReference(link.target, false, action.mode, batchIds, deps)) {
11412
+ return REJECTION_REASON.ACTIVITY_NOT_FOUND;
11413
+ }
11414
+ if (link.linkId !== void 0) {
11415
+ const linkId = String(link.linkId);
11416
+ if (linkId.length === 0 || explicitLinkIds.has(linkId) || action.mode === "append" && deps.adapter.getLink(linkId) !== null) {
11417
+ return REJECTION_REASON.INVALID;
11418
+ }
11419
+ explicitLinkIds.add(linkId);
11420
+ }
11421
+ }
11422
+ return null;
11423
+ }
11424
+ function validReference(reference, allowRoot, mode, batchIds, deps) {
11425
+ if (typeof reference === "string") {
11426
+ if (reference === "0") return allowRoot;
11427
+ return mode === "append" && deps.adapter.getActivity(String(reference)) !== null;
11428
+ }
11429
+ return reference !== null && reference.batchId.length > 0 && batchIds.has(reference.batchId);
11430
+ }
11431
+ function hasParentCycle(items) {
11432
+ const parentByBatchId = /* @__PURE__ */ new Map();
11433
+ for (const item of items) {
11434
+ if (typeof item.parent !== "string") {
11435
+ parentByBatchId.set(item.batchId, item.parent.batchId);
11436
+ }
11437
+ }
11438
+ const verified = /* @__PURE__ */ new Set();
11439
+ for (const item of items) {
11440
+ const path = /* @__PURE__ */ new Set();
11441
+ let current = item.batchId;
11442
+ while (current !== void 0 && !verified.has(current)) {
11443
+ if (path.has(current)) return true;
11444
+ path.add(current);
11445
+ current = parentByBatchId.get(current);
11446
+ }
11447
+ for (const batchId of path) verified.add(batchId);
11448
+ }
11449
+ return false;
11450
+ }
11451
+ function buildBatchPlan(action, deps) {
11452
+ for (const item of action.items) {
11453
+ if (item.activityId !== void 0) {
11454
+ deps.activityIdGen.bump(item.activityId);
11455
+ }
11456
+ }
11457
+ const activityIdsByBatchId = /* @__PURE__ */ new Map();
11458
+ for (const item of action.items) {
11459
+ activityIdsByBatchId.set(
11460
+ item.batchId,
11461
+ item.activityId ?? deps.activityIdGen.next()
11462
+ );
11463
+ }
11464
+ for (const link of action.links) {
11465
+ if (link.linkId !== void 0) deps.linkIdGen.bump(link.linkId);
11466
+ }
11467
+ const linkIds = action.links.map(
11468
+ (link) => link.linkId ?? deps.linkIdGen.next()
11469
+ );
11470
+ const insertionCorrelativeIds = planInsertionCorrelatives(
11471
+ action,
11472
+ activityIdsByBatchId,
11473
+ deps
11474
+ );
11475
+ const items = [];
11476
+ for (const item of action.items) {
11477
+ const activityId = activityIdsByBatchId.get(item.batchId);
11478
+ const insertionCorrelativeId = insertionCorrelativeIds.get(item.batchId);
11479
+ if (activityId === void 0 || insertionCorrelativeId === void 0) {
11480
+ return { ok: false, reason: REJECTION_REASON.INVALID };
11481
+ }
11482
+ const parentBatchId = typeof item.parent === "string" ? null : item.parent.batchId;
11483
+ const parentId = typeof item.parent === "string" ? item.parent : activityIdsByBatchId.get(item.parent.batchId);
11484
+ if (parentId === void 0) {
11485
+ return { ok: false, reason: REJECTION_REASON.PARENT_NOT_FOUND };
11486
+ }
11487
+ items.push({
11488
+ input: item,
11489
+ activityId,
11490
+ parentId,
11491
+ parentBatchId,
11492
+ insertionCorrelativeId
11493
+ });
11494
+ }
11495
+ const links = [];
11496
+ for (const [index, link] of action.links.entries()) {
11497
+ const source = resolveActivityReference(link.source, activityIdsByBatchId);
11498
+ const target = resolveActivityReference(link.target, activityIdsByBatchId);
11499
+ const linkId = linkIds[index];
11500
+ if (source === null || target === null || linkId === void 0) {
11501
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11502
+ }
11503
+ links.push({
11504
+ source,
11505
+ target,
11506
+ type: link.type,
11507
+ lag: link.lag,
11508
+ linkId
11509
+ });
11510
+ }
11511
+ return { ok: true, plan: { items, links } };
11512
+ }
11513
+ function planInsertionCorrelatives(action, activityIdsByBatchId, deps) {
11514
+ if (isSimpleRootAppend(action)) {
11515
+ const firstCorrelativeId = deps.adapter.activityCount();
11516
+ return new Map(
11517
+ action.items.map((item, index) => [
11518
+ item.batchId,
11519
+ firstCorrelativeId + index
11520
+ ])
11521
+ );
11522
+ }
11523
+ const newIds = new Set(
11524
+ Array.from(activityIdsByBatchId.values(), (id) => String(id))
11525
+ );
11526
+ const plansByParent = /* @__PURE__ */ new Map();
11527
+ for (const item of action.items) {
11528
+ const activityId = activityIdsByBatchId.get(item.batchId);
11529
+ if (activityId === void 0) continue;
11530
+ const parentId = typeof item.parent === "string" ? item.parent : activityIdsByBatchId.get(item.parent.batchId);
11531
+ if (parentId === void 0) continue;
11532
+ const parentKey = String(parentId);
11533
+ let plan = plansByParent.get(parentKey);
11534
+ if (!plan) {
11535
+ const existing = action.mode === "replace" || typeof item.parent !== "string" && activityIdsByBatchId.has(item.parent.batchId) ? [] : collectSiblings(deps.adapter, parentKey).sort((left, right) => {
11536
+ const diff = readCorrelativeId(left) - readCorrelativeId(right);
11537
+ return diff || String(left.id).localeCompare(String(right.id));
11538
+ }).map((activity) => String(activity.id));
11539
+ plan = { existing, newIdsByTarget: /* @__PURE__ */ new Map() };
11540
+ plansByParent.set(parentKey, plan);
11541
+ }
11542
+ const target = item.index === void 0 ? plan.existing.length : Math.min(item.index, plan.existing.length);
11543
+ const idsAtTarget = plan.newIdsByTarget.get(target) ?? [];
11544
+ idsAtTarget.push(String(activityId));
11545
+ plan.newIdsByTarget.set(target, idsAtTarget);
11546
+ }
11547
+ const result = /* @__PURE__ */ new Map();
11548
+ const batchIdByActivityId = new Map(
11549
+ Array.from(activityIdsByBatchId, ([batchId, activityId]) => [
11550
+ String(activityId),
11551
+ batchId
11552
+ ])
11553
+ );
11554
+ for (const plan of plansByParent.values()) {
11555
+ const order = [];
11556
+ for (let target = 0; target <= plan.existing.length; target += 1) {
11557
+ for (const newId of plan.newIdsByTarget.get(target) ?? []) {
11558
+ order.push(newId);
11559
+ }
11560
+ const existingId = plan.existing[target];
11561
+ if (existingId !== void 0) order.push(existingId);
11562
+ }
11563
+ let index = 0;
11564
+ while (index < order.length) {
11565
+ if (!newIds.has(order[index])) {
11566
+ index += 1;
11567
+ continue;
11568
+ }
11569
+ const start = index;
11570
+ while (index < order.length && newIds.has(order[index])) index += 1;
11571
+ const count = index - start;
11572
+ const left = start > 0 ? activityCorrelative(order[start - 1], deps) : null;
11573
+ const right = index < order.length ? activityCorrelative(order[index], deps) : null;
11574
+ for (let offset = 0; offset < count; offset += 1) {
11575
+ const id = order[start + offset];
11576
+ const batchId = batchIdByActivityId.get(id);
11577
+ if (batchId === void 0) continue;
11578
+ result.set(
11579
+ batchId,
11580
+ interpolateCorrelative(left, right, offset + 1, count)
11581
+ );
11582
+ }
11583
+ }
11584
+ }
11585
+ return result;
11586
+ }
11587
+ function isSimpleRootAppend(action) {
11588
+ return action.mode === "append" && action.items.every(
11589
+ (item) => item.parent === "0" && item.index === void 0
11590
+ );
11591
+ }
11592
+ function activityCorrelative(activityId, deps) {
11593
+ const activity = deps.adapter.getActivity(activityId);
11594
+ return activity ? readCorrelativeId(activity) : 0;
11595
+ }
11596
+ function interpolateCorrelative(left, right, position, count) {
11597
+ if (left !== null && right !== null && right > left) {
11598
+ return left + (right - left) * position / (count + 1);
11599
+ }
11600
+ if (left !== null) return left + position;
11601
+ if (right !== null) return right - (count - position + 1);
11602
+ return position - 1;
11603
+ }
11604
+ async function createPlannedActivities(action, plannedItems, deps) {
11605
+ const childrenByParent = /* @__PURE__ */ new Map();
11606
+ const ready = [];
11607
+ for (const item of plannedItems) {
11608
+ if (item.parentBatchId === null) {
11609
+ ready.push(item);
11610
+ continue;
11611
+ }
11612
+ const children = childrenByParent.get(item.parentBatchId) ?? [];
11613
+ children.push(item);
11614
+ childrenByParent.set(item.parentBatchId, children);
11615
+ }
11616
+ for (let index = 0; index < ready.length; index += 1) {
11617
+ const item = ready[index];
11618
+ const core = await createActivityCore(
11619
+ {
11620
+ parentId: item.parentId,
11621
+ activityId: item.activityId,
11622
+ ...item.input.overrides === void 0 ? {} : { overrides: item.input.overrides },
11623
+ ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
11624
+ },
11625
+ deps,
11626
+ {
11627
+ skipCorrelativeRecompute: true,
11628
+ skipBeforeSnap: true,
11629
+ skipCacheInvalidation: true,
11630
+ skipRollupCascades: true,
11631
+ insertionCorrelativeId: item.insertionCorrelativeId
11632
+ }
11633
+ );
11634
+ if (!core.ok) return core;
11635
+ for (const child of childrenByParent.get(item.input.batchId) ?? []) {
11636
+ ready.push(child);
11637
+ }
11638
+ }
11639
+ if (ready.length !== plannedItems.length) {
11640
+ return { ok: false, reason: REJECTION_REASON.INVALID };
11641
+ }
11642
+ const recomputedParents = /* @__PURE__ */ new Set();
11643
+ for (let index = ready.length - 1; index >= 0; index -= 1) {
11644
+ const parentId = ready[index].parentId;
11645
+ if (parentId === "0" || recomputedParents.has(String(parentId))) continue;
11646
+ recomputedParents.add(String(parentId));
11647
+ recomputeRollupCascadesForParent(parentId, deps.adapter);
11648
+ }
11649
+ return { ok: true };
11650
+ }
11651
+ function resolveActivityReference(reference, activityIdsByBatchId) {
11652
+ if (typeof reference === "string") return reference;
11653
+ return activityIdsByBatchId.get(reference.batchId) ?? null;
11654
+ }
11655
+ function snapshotAllLinks(deps) {
11656
+ return new Map(
11657
+ deps.adapter.getAllLinks().map((link) => [String(link.id), { ...link }])
11658
+ );
11659
+ }
11660
+ function stringKeyedLinks(links) {
11661
+ return new Map(Array.from(links, ([linkId, link]) => [String(linkId), link]));
11662
+ }
11663
+
11664
+ // src/dispatch/handlers/activity-paste.ts
11665
+ function resolvePasteRootDestination(destination, adapter) {
11666
+ if ("afterSiblingId" in destination) {
11667
+ const sibling = adapter.getActivity(destination.afterSiblingId);
11668
+ if (!sibling) {
11669
+ return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
11670
+ }
11671
+ const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
11672
+ return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
11673
+ }
11674
+ const parentId = destination.parentId;
11675
+ const siblings = collectSiblings(adapter, parentId).sort(
11676
+ (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
11677
+ );
11678
+ const before = siblings[destination.index]?.id;
11679
+ return { ok: true, parentId, firstBefore: before };
11680
+ }
11681
+ function applyPasteDomainSemantics(overrides) {
11682
+ const { customId: _customId, ...content } = overrides;
11683
+ return {
11684
+ ...content,
11685
+ progress: 0,
11686
+ ponderator: 0,
11687
+ usedCost: 0,
11688
+ workHours: 0,
11689
+ isLookahead: false
11690
+ };
11691
+ }
11692
+ async function dispatchActivityPaste(action, options, deps) {
11693
+ const { adapter, scheduler, sector } = deps;
11694
+ const rootDest = resolvePasteRootDestination(action.destination, adapter);
11695
+ if (!rootDest.ok) return rootDest;
11696
+ const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
11697
+ const touched = /* @__PURE__ */ new Set();
11698
+ const originalToNew = /* @__PURE__ */ new Map();
11699
+ const createdIds = [];
11700
+ let prevRootId;
11701
+ for (const input of action.activities) {
11702
+ const mappedParent = originalToNew.get(input.originalParentId);
11703
+ let parentId;
11704
+ let afterSiblingId;
11705
+ let beforeSiblingId;
11706
+ if (mappedParent !== void 0) {
11707
+ parentId = mappedParent;
11708
+ } else {
11709
+ parentId = rootDest.parentId;
11710
+ if (prevRootId === void 0) {
11711
+ afterSiblingId = rootDest.firstAfter;
11712
+ beforeSiblingId = rootDest.firstBefore;
11713
+ } else {
11714
+ afterSiblingId = prevRootId;
11715
+ }
11716
+ }
11717
+ const core = await createActivityCore(
11718
+ {
11719
+ parentId,
11720
+ afterSiblingId,
11721
+ beforeSiblingId,
11722
+ ...input.activityId ? { activityId: input.activityId } : {},
11723
+ overrides: applyPasteDomainSemantics(input.overrides),
11724
+ eventSource: action.eventSource
11725
+ },
11726
+ deps,
11727
+ {
11728
+ skipCorrelativeRecompute: true,
11729
+ skipBeforeSnap: true,
11730
+ customIdReferenceId: action.referenceActivityId
11731
+ }
11732
+ );
11733
+ if (!core.ok) return core;
11734
+ if (input.baselineSnapshot !== void 0) {
11735
+ const snapshot = input.baselineSnapshot;
11736
+ adapter.setActivityField(
11737
+ core.newId,
11738
+ "baselineSnapshot",
11739
+ snapshot ? {
11740
+ ...snapshot,
11741
+ startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
11742
+ endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
11743
+ } : null
11744
+ );
11745
+ }
11746
+ originalToNew.set(input.originalId, core.newId);
11747
+ createdIds.push(core.newId);
11748
+ if (mappedParent === void 0) prevRootId = core.newId;
11749
+ }
11750
+ const correlativeShifts = recomputeCorrelativeIds(adapter);
11751
+ const createdIdSet = new Set(createdIds.map(String));
11752
+ foldCorrelativeShifts(
11753
+ adapter,
11754
+ correlativeShifts,
11755
+ createdIdSet,
11756
+ beforeSnap,
11757
+ touched
11758
+ );
11759
+ for (const createdId of createdIds) touched.add(String(createdId));
11760
+ const appliedLinks = [];
11761
+ for (const link of action.links) {
11762
+ const newSource = originalToNew.get(link.source);
11763
+ const newTarget = originalToNew.get(link.target);
11764
+ if (newSource === void 0 || newTarget === void 0) continue;
11765
+ const op = {
11766
+ kind: "create",
11767
+ source: newSource,
11768
+ target: newTarget,
11769
+ type: link.type,
11770
+ lag: lagDaysToHours(link.lag, sector.hoursPerDay)
11771
+ };
11772
+ const res = applyLinkOperation(op, {
11773
+ port: adapter,
11774
+ newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
11775
+ });
11776
+ appliedLinks.push({
11777
+ op,
11778
+ finalLinkId: res.applied ? res.linkId : null,
11779
+ rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
11780
+ });
11781
+ }
11782
+ const { scheduledIds } = await runPostMutation(
11783
+ {
11784
+ adapter,
11785
+ scheduler,
11786
+ sector: deps.sector,
11787
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
11788
+ },
11789
+ {
11790
+ action,
11791
+ autoscheduleFrom: createdIds[0] ?? null,
11792
+ recomputeParentsFrom: createdIds,
11793
+ now: deps.now,
11794
+ options
11795
+ }
11796
+ );
11797
+ const linkChanges = buildLinkChangesForBatch(
11798
+ appliedLinks,
11799
+ /* @__PURE__ */ new Map(),
11800
+ adapter
11801
+ );
11802
+ return {
11803
+ ok: true,
11804
+ changes: await assembleChangeSet(adapter, {
11805
+ source: action,
11806
+ beforeSnap,
11807
+ touchedIds: touched,
11808
+ scheduledIds,
11809
+ links: linkChanges,
11810
+ hoursPerDay: sector.hoursPerDay
11811
+ })
11812
+ };
11813
+ }
11814
+
11815
+ // src/dispatch/handlers/sir-sync.ts
11816
+ function dispatchSirSync(action, deps) {
11817
+ const { adapter } = deps;
11818
+ const id = action.activityId;
11819
+ const snapshot = adapter.getActivity(id);
11820
+ if (!snapshot) {
11821
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11822
+ }
11823
+ const before = snapshot.pendingRequestIds ?? [];
11824
+ const after = action.pendingRequests.map((r) => r.id);
11825
+ adapter.setActivityField(action.activityId, "pendingRequestIds", after);
11826
+ const change = {
11827
+ id,
11828
+ kind: "updated",
11829
+ fields: { pendingRequestIds: { before, after } },
11830
+ after: { id, pendingRequestIds: after }
11831
+ };
11832
+ return {
11833
+ ok: true,
11834
+ changes: {
11835
+ source: action,
11836
+ activities: [change],
11837
+ links: [],
11838
+ calendars: [],
11839
+ trackingEvents: []
11840
+ }
11841
+ };
11842
+ }
11843
+
11844
+ // src/dispatch/handlers/activity-lookahead-sync.ts
11845
+ function dispatchActivityLookaheadSync(action, deps) {
11846
+ const ids = [...new Set(action.activityIds.map(String))];
11847
+ const snapshots = ids.map((id) => deps.adapter.getActivity(id));
11848
+ if (snapshots.some((snapshot) => !snapshot)) {
11849
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11850
+ }
11851
+ const activities = [];
11852
+ for (const [index, id] of ids.entries()) {
11853
+ const snapshot = snapshots[index];
11854
+ if (!snapshot) continue;
11855
+ const fields = {};
11856
+ if (snapshot.isLookahead !== action.isLookahead) {
11857
+ fields.isLookahead = {
11858
+ before: snapshot.isLookahead,
11859
+ after: action.isLookahead
11860
+ };
11861
+ deps.adapter.setActivityField(id, "isLookahead", action.isLookahead);
11862
+ }
11863
+ if (snapshot.hasLookaheadTasks !== action.hasLookaheadTasks) {
11864
+ fields.hasLookaheadTasks = {
11865
+ before: snapshot.hasLookaheadTasks,
11866
+ after: action.hasLookaheadTasks
11867
+ };
11868
+ deps.adapter.setActivityField(
11869
+ id,
11870
+ "hasLookaheadTasks",
11871
+ action.hasLookaheadTasks
11872
+ );
11873
+ }
11874
+ if (Object.keys(fields).length === 0) continue;
11875
+ activities.push({
11876
+ id,
11877
+ kind: "updated",
11878
+ fields,
11879
+ after: {
11880
+ ...snapshot,
11881
+ isLookahead: action.isLookahead,
11882
+ hasLookaheadTasks: action.hasLookaheadTasks
11883
+ }
11884
+ });
11885
+ }
11886
+ return {
11887
+ ok: true,
11888
+ changes: {
11889
+ source: action,
11890
+ activities,
11891
+ links: [],
11892
+ calendars: [],
11893
+ trackingEvents: []
11894
+ }
11895
+ };
11194
11896
  }
11195
11897
 
11196
11898
  // src/internal/hierarchy/invalid-links.ts
@@ -12788,7 +13490,10 @@ async function dispatchStatusCriteriaSet(action, deps) {
12788
13490
  }
12789
13491
 
12790
13492
  // src/dispatch/dispatch.ts
12791
- async function dispatch(action, options, deps) {
13493
+ async function performDispatch(action, options, deps) {
13494
+ if (action.kind === "activity-batch") {
13495
+ return dispatchActivityBatch(action, options, deps);
13496
+ }
12792
13497
  if (action.kind === "persistence-acknowledge") {
12793
13498
  return dispatchPersistenceAcknowledge(action, deps);
12794
13499
  }
@@ -16022,6 +16727,7 @@ var ScheduleState = class {
16022
16727
  };
16023
16728
  _writeCapture = new WriteCapture();
16024
16729
  _viewState = new ViewStateStore();
16730
+ _filterProjection = null;
16025
16731
  _viewStateBefore = null;
16026
16732
  _lastStartDateBefore = null;
16027
16733
  _promotionSnapshotBefore = null;
@@ -16136,6 +16842,13 @@ var ScheduleState = class {
16136
16842
  setActiveFilter(nextFilter) {
16137
16843
  this._captureViewStateOnce();
16138
16844
  this._viewState.setActiveFilter(nextFilter);
16845
+ this._filterProjection = null;
16846
+ }
16847
+ getFilterProjection() {
16848
+ return this._filterProjection;
16849
+ }
16850
+ setFilterProjection(projection) {
16851
+ this._filterProjection = projection;
16139
16852
  }
16140
16853
  getActiveOrder() {
16141
16854
  return this._viewState.getActiveOrder();
@@ -16177,6 +16890,7 @@ var ScheduleState = class {
16177
16890
  }
16178
16891
  hydrateViewState(seed2) {
16179
16892
  this._viewState.hydrate(seed2);
16893
+ this._filterProjection = null;
16180
16894
  this.invalidateDerivedOrder();
16181
16895
  }
16182
16896
  _captureViewStateOnce() {
@@ -16377,6 +17091,7 @@ var ScheduleState = class {
16377
17091
  this._restoreActivityFields(before);
16378
17092
  if (this._viewStateBefore !== null) {
16379
17093
  this._viewState.restore(this._viewStateBefore);
17094
+ this._filterProjection = null;
16380
17095
  this.invalidateDerivedOrder();
16381
17096
  }
16382
17097
  if (this._promotionSnapshotBefore !== null) {
@@ -16586,8 +17301,10 @@ function buildHistoryChangeSet(changes) {
16586
17301
  ...entry,
16587
17302
  after: null
16588
17303
  }));
17304
+ const source = changes.source.kind === "activity-batch" ? { ...changes.source, items: [], links: [] } : changes.source;
16589
17305
  return cloneDomainValue({
16590
17306
  ...changes,
17307
+ source,
16591
17308
  activities: activitiesWithoutAfter,
16592
17309
  links: linksWithoutAfter
16593
17310
  });
@@ -16604,7 +17321,7 @@ function captureCreatedSnapshots(state, changeSet) {
16604
17321
  if (live) afterSnap.set(String(ch.id), structuredCloneActivity(live));
16605
17322
  }
16606
17323
  for (const ch of changeSet.links) {
16607
- if (ch.kind !== "created") continue;
17324
+ if (ch.kind === "deleted") continue;
16608
17325
  const live = state.getLink(String(ch.id));
16609
17326
  if (live) afterLinks.set(String(ch.id), { ...live });
16610
17327
  }
@@ -16827,6 +17544,17 @@ function applyChangeSetSide(state, entry, side) {
16827
17544
  for (const ch of cs.links) {
16828
17545
  if (ch.kind === "updated") {
16829
17546
  if (!ch.fields) continue;
17547
+ if (Object.hasOwn(ch.fields, "source") || Object.hasOwn(ch.fields, "target")) {
17548
+ const snapshot = side === "before" ? entry.beforeLinks?.get(String(ch.id)) : entry.afterLinks?.get(String(ch.id));
17549
+ if (!snapshot) {
17550
+ throw new Error(
17551
+ `${side === "before" ? "undo" : "redo"}: missing link snapshot for ${ch.id}`
17552
+ );
17553
+ }
17554
+ state.removeLink(String(ch.id));
17555
+ state.addLink({ ...snapshot });
17556
+ continue;
17557
+ }
16830
17558
  for (const [field, fv] of Object.entries(ch.fields)) {
16831
17559
  setLinkFieldDynamic(
16832
17560
  state,
@@ -17097,9 +17825,26 @@ function idArrayEquals(current, saved) {
17097
17825
  return currentIds.every((value, index) => value === savedIds[index]);
17098
17826
  }
17099
17827
 
17828
+ // src/boundary/save/link-changes.ts
17829
+ function linkChangedSince(baseline, current) {
17830
+ return baseline.source !== current.source || baseline.target !== current.target || baseline.lag !== current.lag || baseline.type !== current.type;
17831
+ }
17832
+ function checkNoUpdatedLinks(baseline, current) {
17833
+ const baselineById = new Map(
17834
+ baseline.map((link) => [String(link.id), link])
17835
+ );
17836
+ return current.filter((link) => {
17837
+ const base = baselineById.get(String(link.id));
17838
+ if (!base) return false;
17839
+ return linkChangedSince(base, link);
17840
+ });
17841
+ }
17842
+
17100
17843
  // src/boundary/save/save-tracker.ts
17101
17844
  var SaveTracker = class {
17102
17845
  initialized = false;
17846
+ dirtyActivityIds = /* @__PURE__ */ new Set();
17847
+ dirtyLinkIds = /* @__PURE__ */ new Set();
17103
17848
  linksAtLastSave = /* @__PURE__ */ new Map();
17104
17849
  activitiesAtLastSave = /* @__PURE__ */ new Map();
17105
17850
  init(activities, links) {
@@ -17109,10 +17854,16 @@ var SaveTracker = class {
17109
17854
  }
17110
17855
  snapshotActivities(activities) {
17111
17856
  this.activitiesAtLastSave.clear();
17857
+ this.dirtyActivityIds.clear();
17112
17858
  for (const activity of activities) {
17113
- if (activity.proplannerId == null) continue;
17859
+ const activityId = String(activity.id);
17860
+ if (activity.proplannerId == null) {
17861
+ this.dirtyActivityIds.add(activityId);
17862
+ continue;
17863
+ }
17864
+ const persistable = projectPersistableActivity(activity);
17114
17865
  this.activitiesAtLastSave.set(String(activity.id), {
17115
- persistable: projectPersistableActivity(activity),
17866
+ persistable,
17116
17867
  deletedSnapshot: {
17117
17868
  id: activity.id,
17118
17869
  proplannerId: activity.proplannerId,
@@ -17120,12 +17871,20 @@ var SaveTracker = class {
17120
17871
  hadBaseline: activity.baselineSnapshot !== null || activity.baselinePoints.length > 0
17121
17872
  }
17122
17873
  });
17874
+ if (activityChangedSince(activity, persistable)) {
17875
+ this.dirtyActivityIds.add(activityId);
17876
+ }
17123
17877
  }
17124
17878
  }
17125
17879
  snapshotLinks(links) {
17126
17880
  this.linksAtLastSave.clear();
17881
+ this.dirtyLinkIds.clear();
17127
17882
  for (const link of links) {
17128
- if (link.proplannerId == null) continue;
17883
+ const linkId = String(link.id);
17884
+ if (link.proplannerId == null) {
17885
+ this.dirtyLinkIds.add(linkId);
17886
+ continue;
17887
+ }
17129
17888
  this.linksAtLastSave.set(String(link.id), {
17130
17889
  id: link.id,
17131
17890
  proplannerId: link.proplannerId,
@@ -17136,21 +17895,52 @@ var SaveTracker = class {
17136
17895
  });
17137
17896
  }
17138
17897
  }
17139
- hasUnsavedChanges(activities, links) {
17140
- if (!this.initialized) return false;
17141
- if (this.deletedActivities(activities).length > 0) return true;
17142
- if (this.deletedLinks(links).length > 0) return true;
17143
- const activityChanged = activities.some((activity) => {
17144
- if (activity.proplannerId == null) return true;
17145
- const saved = this.activitiesAtLastSave.get(String(activity.id));
17146
- return !saved || activityChangedSince(activity, saved.persistable);
17147
- });
17148
- if (activityChanged) return true;
17149
- return links.some((link) => {
17150
- if (link.proplannerId == null) return true;
17151
- const saved = this.linksAtLastSave.get(String(link.id));
17152
- return !saved || link.lag !== saved.lag || link.type !== saved.type;
17153
- });
17898
+ recordChanges(activities, links, getActivity, getLink) {
17899
+ if (!this.initialized) return;
17900
+ for (const change of activities) {
17901
+ const activityId = String(change.id);
17902
+ const current = getActivity(activityId);
17903
+ const saved = this.activitiesAtLastSave.get(activityId);
17904
+ if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || activityChangedSince(current, saved.persistable)) {
17905
+ this.dirtyActivityIds.add(activityId);
17906
+ } else {
17907
+ this.dirtyActivityIds.delete(activityId);
17908
+ }
17909
+ }
17910
+ for (const change of links) {
17911
+ const linkId = String(change.id);
17912
+ const current = getLink(linkId);
17913
+ const saved = this.linksAtLastSave.get(linkId);
17914
+ if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || linkChangedSince(saved, current)) {
17915
+ this.dirtyLinkIds.add(linkId);
17916
+ } else {
17917
+ this.dirtyLinkIds.delete(linkId);
17918
+ }
17919
+ }
17920
+ }
17921
+ acknowledgePersistedLookahead(activityIds, getActivity) {
17922
+ if (!this.initialized) return;
17923
+ for (const activityId of activityIds) {
17924
+ const current = getActivity(activityId);
17925
+ const saved = this.activitiesAtLastSave.get(activityId);
17926
+ if (current === null || saved === void 0) continue;
17927
+ const persistable = {
17928
+ ...saved.persistable,
17929
+ isLookahead: current.isLookahead
17930
+ };
17931
+ this.activitiesAtLastSave.set(activityId, {
17932
+ ...saved,
17933
+ persistable
17934
+ });
17935
+ if (current.proplannerId == null || activityChangedSince(current, persistable)) {
17936
+ this.dirtyActivityIds.add(activityId);
17937
+ } else {
17938
+ this.dirtyActivityIds.delete(activityId);
17939
+ }
17940
+ }
17941
+ }
17942
+ hasUnsavedChanges() {
17943
+ return this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
17154
17944
  }
17155
17945
  deletedActivities(activities) {
17156
17946
  if (!this.initialized) return [];
@@ -17188,10 +17978,13 @@ var SaveTracker = class {
17188
17978
  if (link.proplannerId == null) return false;
17189
17979
  const saved = this.linksAtLastSave.get(String(link.id));
17190
17980
  if (!saved) return true;
17191
- return link.lag !== saved.lag || link.type !== saved.type;
17981
+ return linkChangedSince(saved, link);
17192
17982
  });
17193
17983
  }
17194
17984
  clear() {
17985
+ this.initialized = false;
17986
+ this.dirtyActivityIds.clear();
17987
+ this.dirtyLinkIds.clear();
17195
17988
  this.linksAtLastSave.clear();
17196
17989
  this.activitiesAtLastSave.clear();
17197
17990
  }
@@ -17793,6 +18586,12 @@ function createActivityIdGenerator(options = {}) {
17793
18586
  const n = typeof id === "number" ? id : Number(id);
17794
18587
  if (!Number.isFinite(n)) return;
17795
18588
  raise(n);
18589
+ },
18590
+ snapshot() {
18591
+ return counter;
18592
+ },
18593
+ restore(snapshot) {
18594
+ counter = snapshot;
17796
18595
  }
17797
18596
  };
17798
18597
  }
@@ -17818,6 +18617,12 @@ function createLinkIdGenerator(options = {}) {
17818
18617
  const n = typeof id === "number" ? id : Number(id);
17819
18618
  if (!Number.isFinite(n)) return;
17820
18619
  raise(n);
18620
+ },
18621
+ snapshot() {
18622
+ return counter;
18623
+ },
18624
+ restore(snapshot) {
18625
+ counter = snapshot;
17821
18626
  }
17822
18627
  };
17823
18628
  }
@@ -17847,6 +18652,12 @@ function createUniqueCorrelativeIdGenerator(options = {}) {
17847
18652
  if (uid2 == null) return;
17848
18653
  const n = typeof uid2 === "number" ? uid2 : Number(uid2);
17849
18654
  raise(n);
18655
+ },
18656
+ snapshot() {
18657
+ return counter;
18658
+ },
18659
+ restore(snapshot) {
18660
+ counter = snapshot;
17850
18661
  }
17851
18662
  };
17852
18663
  }
@@ -17923,13 +18734,13 @@ function minSuffixFromArray(suffixes) {
17923
18734
  if (!suffixes || suffixes.length === 0) {
17924
18735
  throw new Error("minSuffixFromArray requires non-empty array");
17925
18736
  }
17926
- return suffixes.reduce((min, current) => minSuffix(min, current));
18737
+ return suffixes.reduce((min2, current) => minSuffix(min2, current));
17927
18738
  }
17928
18739
  function maxSuffixFromArray(suffixes) {
17929
18740
  if (!suffixes || suffixes.length === 0) {
17930
18741
  throw new Error("maxSuffixFromArray requires non-empty array");
17931
18742
  }
17932
- return suffixes.reduce((max, current) => maxSuffix(max, current));
18743
+ return suffixes.reduce((max2, current) => maxSuffix(max2, current));
17933
18744
  }
17934
18745
 
17935
18746
  // src/generators/custom-id/generation-strategies.ts
@@ -18623,13 +19434,204 @@ function initializeCore(input) {
18623
19434
  };
18624
19435
  }
18625
19436
 
19437
+ // src/propagations/upward/date-gesture-preview.ts
19438
+ var emptyFormula = () => ({
19439
+ constantStart: null,
19440
+ variableStartOffset: null,
19441
+ constantEnd: null,
19442
+ variableEndOffset: null
19443
+ });
19444
+ var min = (left, right) => left === null ? right : Math.min(left, right);
19445
+ var max = (left, right) => left === null ? right : Math.max(left, right);
19446
+ function createDateGesturePreview(adapter, gesture, assertCurrent) {
19447
+ const participantIds = [
19448
+ ...new Set(
19449
+ gesture.kind === "move" ? gesture.activityIds.map(String) : [String(gesture.activityId)]
19450
+ )
19451
+ ];
19452
+ if (participantIds.length === 0) {
19453
+ throw new Error("[ScheduleCore] date gesture has no participants");
19454
+ }
19455
+ const formulas = buildParticipantFormulas(adapter, gesture, participantIds);
19456
+ addAncestorFormulas(adapter, participantIds, formulas);
19457
+ return {
19458
+ getDates(activityId, proposedEdgeTimeMs) {
19459
+ assertCurrent();
19460
+ if (!Number.isFinite(proposedEdgeTimeMs)) {
19461
+ throw new Error("[ScheduleCore] invalid date gesture edge");
19462
+ }
19463
+ const formula = formulas.get(String(activityId));
19464
+ if (!formula) return null;
19465
+ const startTimeMs = evaluateStart(formula, proposedEdgeTimeMs);
19466
+ const endTimeMs = evaluateEnd(formula, proposedEdgeTimeMs);
19467
+ if (startTimeMs === null || endTimeMs === null) {
19468
+ throw new Error(
19469
+ `[ScheduleCore] incomplete date gesture bounds for ${String(activityId)}`
19470
+ );
19471
+ }
19472
+ return { startTimeMs, endTimeMs };
19473
+ }
19474
+ };
19475
+ }
19476
+ function buildParticipantFormulas(adapter, gesture, participantIds) {
19477
+ const formulas = /* @__PURE__ */ new Map();
19478
+ const anchorId = gesture.kind === "move" ? String(gesture.anchorActivityId) : null;
19479
+ const anchor = anchorId === null ? null : requireLeafActivity(adapter, anchorId);
19480
+ if (anchorId !== null && !participantIds.includes(anchorId)) {
19481
+ throw new Error("[ScheduleCore] move anchor is not a participant");
19482
+ }
19483
+ for (const activityId of participantIds) {
19484
+ const activity = requireLeafActivity(adapter, activityId);
19485
+ if (gesture.kind === "move") {
19486
+ const anchorStart = anchor.startDate.getTime();
19487
+ formulas.set(activityId, {
19488
+ constantStart: null,
19489
+ variableStartOffset: activity.startDate.getTime() - anchorStart,
19490
+ constantEnd: null,
19491
+ variableEndOffset: activity.endDate.getTime() - anchorStart
19492
+ });
19493
+ } else if (gesture.kind === "resize-start") {
19494
+ formulas.set(activityId, {
19495
+ constantStart: null,
19496
+ variableStartOffset: 0,
19497
+ constantEnd: activity.endDate.getTime(),
19498
+ variableEndOffset: null
19499
+ });
19500
+ } else {
19501
+ formulas.set(activityId, {
19502
+ constantStart: activity.startDate.getTime(),
19503
+ variableStartOffset: null,
19504
+ constantEnd: null,
19505
+ variableEndOffset: 0
19506
+ });
19507
+ }
19508
+ }
19509
+ return formulas;
19510
+ }
19511
+ function addAncestorFormulas(adapter, participantIds, formulas) {
19512
+ const ancestors = findAncestorsOfDirty(adapter, new Set(participantIds));
19513
+ for (const parentId of ancestors) {
19514
+ const formula = emptyFormula();
19515
+ for (const childId of adapter.getChildren(parentId)) {
19516
+ const childFormula = formulas.get(childId);
19517
+ if (childFormula) {
19518
+ mergeFormula(formula, childFormula);
19519
+ continue;
19520
+ }
19521
+ const child = adapter.getActivity(childId);
19522
+ if (!child) {
19523
+ throw new Error(
19524
+ `[ScheduleCore] missing child ${childId} while previewing parent ${parentId}`
19525
+ );
19526
+ }
19527
+ formula.constantStart = min(
19528
+ formula.constantStart,
19529
+ child.startDate.getTime()
19530
+ );
19531
+ formula.constantEnd = max(formula.constantEnd, child.endDate.getTime());
19532
+ }
19533
+ formulas.set(parentId, formula);
19534
+ }
19535
+ }
19536
+ function requireLeafActivity(adapter, activityId) {
19537
+ const activity = adapter.getActivity(activityId);
19538
+ if (!activity) {
19539
+ throw new Error(
19540
+ `[ScheduleCore] missing date gesture activity ${activityId}`
19541
+ );
19542
+ }
19543
+ if (adapter.hasChildren(activityId)) {
19544
+ throw new Error(
19545
+ `[ScheduleCore] summary activity ${activityId} cannot be dragged directly`
19546
+ );
19547
+ }
19548
+ return activity;
19549
+ }
19550
+ function mergeFormula(target, source) {
19551
+ if (source.constantStart !== null) {
19552
+ target.constantStart = min(target.constantStart, source.constantStart);
19553
+ }
19554
+ if (source.variableStartOffset !== null) {
19555
+ target.variableStartOffset = min(
19556
+ target.variableStartOffset,
19557
+ source.variableStartOffset
19558
+ );
19559
+ }
19560
+ if (source.constantEnd !== null) {
19561
+ target.constantEnd = max(target.constantEnd, source.constantEnd);
19562
+ }
19563
+ if (source.variableEndOffset !== null) {
19564
+ target.variableEndOffset = max(
19565
+ target.variableEndOffset,
19566
+ source.variableEndOffset
19567
+ );
19568
+ }
19569
+ }
19570
+ function evaluateStart(formula, proposedEdgeTimeMs) {
19571
+ if (formula.variableStartOffset === null) return formula.constantStart;
19572
+ const variable = proposedEdgeTimeMs + formula.variableStartOffset;
19573
+ return formula.constantStart === null ? variable : Math.min(formula.constantStart, variable);
19574
+ }
19575
+ function evaluateEnd(formula, proposedEdgeTimeMs) {
19576
+ if (formula.variableEndOffset === null) return formula.constantEnd;
19577
+ const variable = proposedEdgeTimeMs + formula.variableEndOffset;
19578
+ return formula.constantEnd === null ? variable : Math.max(formula.constantEnd, variable);
19579
+ }
19580
+
18626
19581
  // src/init/schedule-core.ts
19582
+ function changesRequireFilterProjectionRebuild(changes) {
19583
+ for (const change of changes.activities) {
19584
+ if (change.kind === "deleted") return true;
19585
+ const hasMovedParent = change.kind === "updated" && change.fields !== void 0 && "parentId" in change.fields;
19586
+ if (hasMovedParent) return true;
19587
+ }
19588
+ return false;
19589
+ }
19590
+ function collectChangedActivities(changes, adapter) {
19591
+ const activities = [];
19592
+ for (const change of changes.activities) {
19593
+ const activity = adapter.getActivity(change.id);
19594
+ if (activity === null) return null;
19595
+ activities.push(activity);
19596
+ }
19597
+ return activities;
19598
+ }
19599
+ function isCurrentFilterProjection(projection, filter, context) {
19600
+ if (projection === null || projection.filter !== filter) return false;
19601
+ const hasCurrentHours = projection.context.hoursPerDay === context.hoursPerDay;
19602
+ const hasCurrentLocale = projection.context.locale === context.locale;
19603
+ return hasCurrentHours && hasCurrentLocale;
19604
+ }
18627
19605
  var ScheduleCore = class {
18628
19606
  _status = SCHEDULE_CORE_STATUS.READY;
18629
19607
  coreRuntime;
18630
19608
  _undo = new UndoRecorder();
18631
19609
  _opQueue = Promise.resolve();
18632
19610
  _scheduleRevision = 0;
19611
+ /**
19612
+ * Revision counting mutations that have been ADMITTED, whether or not they
19613
+ * have changed anything yet. `_scheduleRevision` counts the ones that
19614
+ * actually did.
19615
+ *
19616
+ * INVARIANT: `_pendingScheduleRevision >= _scheduleRevision`, and equality
19617
+ * means no mutation is in flight.
19618
+ *
19619
+ * The two exist separately because the useful instant and the knowable
19620
+ * instant are not the same one. A critical-path calculation becomes garbage
19621
+ * the moment the next mutation is admitted, but whether that mutation is
19622
+ * substantive is only knowable once its ChangeSet exists, which is hundreds
19623
+ * of milliseconds later at project scale. Measured live on a 12268-activity
19624
+ * project: the cancellation flag flipped 929 ms into a 930 ms calculation,
19625
+ * 812 into 812 and 785 into 785, because the job's apply block queues behind
19626
+ * the very dispatch whose completion cancels it. Cancellation and job end
19627
+ * were the same event, so every discarded job ran the whole calculation.
19628
+ *
19629
+ * Splitting the counter buys the early signal without moving the meaning of
19630
+ * `_scheduleRevision`, whose three readers (`isCriticalPathSettled`,
19631
+ * `createDateGesturePreview` and the job's own `isCurrent`) are all written
19632
+ * against "the state actually changed".
19633
+ */
19634
+ _pendingScheduleRevision = 0;
18633
19635
  _criticalPathRevision = -1;
18634
19636
  _activeCriticalPath = null;
18635
19637
  constructor(input) {
@@ -18682,6 +19684,16 @@ var ScheduleCore = class {
18682
19684
  this.assertReady();
18683
19685
  return readActivity(this.coreRuntime.state, id);
18684
19686
  }
19687
+ createDateGesturePreview(gesture) {
19688
+ this.assertReady();
19689
+ const revision = this._scheduleRevision;
19690
+ return createDateGesturePreview(this.coreRuntime.state, gesture, () => {
19691
+ this.assertReady();
19692
+ if (this._scheduleRevision !== revision) {
19693
+ throw new Error("[ScheduleCore] date gesture preview is stale");
19694
+ }
19695
+ });
19696
+ }
18685
19697
  getAllActivitiesView() {
18686
19698
  this.assertReady();
18687
19699
  return readAllActivities(this.coreRuntime.state);
@@ -18779,10 +19791,7 @@ var ScheduleCore = class {
18779
19791
  }
18780
19792
  hasUnsavedChanges() {
18781
19793
  this.assertReady();
18782
- return this._saveTracker.hasUnsavedChanges(
18783
- this.coreRuntime.state.getAllActivities(),
18784
- this.getAllLinksView()
18785
- );
19794
+ return this._saveTracker.hasUnsavedChanges();
18786
19795
  }
18787
19796
  getDeletedActivitiesSinceLastSave() {
18788
19797
  this.assertReady();
@@ -18800,10 +19809,90 @@ var ScheduleCore = class {
18800
19809
  );
18801
19810
  return workPromise;
18802
19811
  }
19812
+ /**
19813
+ * Marks a mutation as admitted and invalidates the calculation in flight.
19814
+ *
19815
+ * Called BEFORE `_enqueue`, which is the point of the whole thing: the wait
19816
+ * in the operation queue is part of the window a running calculation wastes,
19817
+ * and it is the longest part of it when several mutations are already
19818
+ * queued.
19819
+ *
19820
+ * `dispatchChangesSchedulingState` is a pure function of the action, so this
19821
+ * decision needs no state and cannot be wrong about the action's nature. What
19822
+ * it cannot know yet is whether the mutation will produce anything, which is
19823
+ * what `_settleScheduleMutation` reconciles afterwards.
19824
+ */
19825
+ _beginScheduleMutation(action) {
19826
+ if (!dispatchChangesSchedulingState(action)) return false;
19827
+ return this._admitScheduleMutation();
19828
+ }
19829
+ /** Undo and redo have no action to classify: reaching them IS the mutation. */
19830
+ _admitScheduleMutation() {
19831
+ this._pendingScheduleRevision++;
19832
+ if (this._activeCriticalPath) {
19833
+ this._activeCriticalPath.cancellation.cancelled = true;
19834
+ }
19835
+ return true;
19836
+ }
19837
+ /**
19838
+ * Closes a mutation admitted by `_beginScheduleMutation`, on EVERY exit path.
19839
+ *
19840
+ * `changedState` false means the mutation was admitted and produced nothing
19841
+ * (rejected, non-substantive, or rolled back). The pending revision walks
19842
+ * back so the invariant holds and `isCriticalPathSettled` keeps telling the
19843
+ * truth, and the calculation this mutation killed for nothing is re-armed.
19844
+ *
19845
+ * The re-arm goes through the queue and that is not incidental. Started
19846
+ * outside it, the calculation would run immediately while the next queued
19847
+ * mutation is still waiting, and that mutation would kill it again on
19848
+ * admission: a cancel-and-restart treadmill burning a fresh project-wide deep
19849
+ * copy per lap. Inside the queue the restart cannot happen until the queue
19850
+ * drains, so at most one calculation is armed per settled mutation. The burst
19851
+ * scenario will NOT catch a regression here, because there every dispatch
19852
+ * succeeds and this path is never taken.
19853
+ *
19854
+ * It re-arms through `recomputeCriticalPath` rather than enqueueing the job
19855
+ * directly, because the queue slot must NOT await the job: the job's own
19856
+ * apply block needs a later slot on this same queue, so awaiting it from
19857
+ * inside a slot deadlocks the core. `recomputeCriticalPath` already has the
19858
+ * shape that returns the job's promise out of the slot instead of awaiting
19859
+ * it, and it keeps `_criticalPathReady` pointing at the live calculation.
19860
+ */
19861
+ _settleScheduleMutation(admitted, changedState) {
19862
+ if (!admitted) return;
19863
+ if (changedState) return;
19864
+ this._pendingScheduleRevision = Math.max(
19865
+ this._pendingScheduleRevision - 1,
19866
+ this._scheduleRevision
19867
+ );
19868
+ if (this._pendingScheduleRevision !== this._scheduleRevision) return;
19869
+ if (this._criticalPathRevision === this._scheduleRevision) return;
19870
+ void this.recomputeCriticalPath().catch((error) => {
19871
+ this.coreRuntime.reporter.error(
19872
+ "[ScheduleCore] Critical Path re-arm after a barren mutation failed",
19873
+ error
19874
+ );
19875
+ });
19876
+ }
18803
19877
  async dispatch(action, options = {}) {
18804
- return this._enqueue(() => this._dispatchInner(action, options));
19878
+ const admitted = this._beginScheduleMutation(action);
19879
+ return this._enqueue(() => this._dispatchInner(action, options, admitted));
19880
+ }
19881
+ async applyActivityBatch(action, options = {}) {
19882
+ const admitted = this._beginScheduleMutation(action);
19883
+ return this._enqueue(() => this._dispatchInner(action, options, admitted));
19884
+ }
19885
+ async _dispatchInner(action, options = {}, admitted = false) {
19886
+ let changedState = false;
19887
+ try {
19888
+ const dispatched = await this._runDispatch(action, options);
19889
+ changedState = dispatched.changedState;
19890
+ return dispatched.result;
19891
+ } finally {
19892
+ this._settleScheduleMutation(admitted, changedState);
19893
+ }
18805
19894
  }
18806
- async _dispatchInner(action, options = {}) {
19895
+ async _runDispatch(action, options) {
18807
19896
  this.assertReady();
18808
19897
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
18809
19898
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
@@ -18811,11 +19900,16 @@ var ScheduleCore = class {
18811
19900
  inputUnit: options.inputUnit ?? "days"
18812
19901
  });
18813
19902
  const historyPolicy = getDispatchHistoryPolicy(action);
19903
+ const generatorSnapshot = [
19904
+ this.coreRuntime.activityIdGenerator.snapshot(),
19905
+ this.coreRuntime.linkIdGenerator.snapshot(),
19906
+ this.coreRuntime.uniqueCorrelativeIdGenerator.snapshot()
19907
+ ];
18814
19908
  this.coreRuntime.state.beginWriteCapture();
18815
19909
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
18816
19910
  let result;
18817
19911
  try {
18818
- result = await dispatch(action, options, {
19912
+ result = await performDispatch(action, options, {
18819
19913
  adapter: this.coreRuntime.state,
18820
19914
  ctx: pipelineContext,
18821
19915
  scheduler: this.coreRuntime.scheduler,
@@ -18831,9 +19925,9 @@ var ScheduleCore = class {
18831
19925
  )?.id ?? null,
18832
19926
  now: this.coreRuntime.clock ? endOfLocalDay(this.coreRuntime.clock()) : null
18833
19927
  });
18834
- if (!result.ok) this._rollback();
19928
+ if (!result.ok) this._rollback(generatorSnapshot);
18835
19929
  } catch (error) {
18836
- this._rollback();
19930
+ this._rollback(generatorSnapshot);
18837
19931
  throw error;
18838
19932
  } finally {
18839
19933
  this.coreRuntime.state.endWriteCapture();
@@ -18856,19 +19950,36 @@ var ScheduleCore = class {
18856
19950
  Date.now()
18857
19951
  );
18858
19952
  }
18859
- if (result.ok && historyPolicy === "clear-on-success") {
18860
- this._saveTracker.snapshotActivities(
18861
- this.coreRuntime.state.getAllActivities()
18862
- );
18863
- this._saveTracker.snapshotLinks(this.getAllLinksView());
18864
- this._undo.clear();
19953
+ if (result.ok) {
19954
+ if (historyPolicy === "clear-on-success") {
19955
+ this._saveTracker.snapshotActivities(
19956
+ this.coreRuntime.state.getAllActivities()
19957
+ );
19958
+ this._saveTracker.snapshotLinks(this.getAllLinksView());
19959
+ this._undo.clear();
19960
+ } else if (action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true) {
19961
+ this._saveTracker.acknowledgePersistedLookahead(
19962
+ action.activityIds,
19963
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
19964
+ );
19965
+ } else {
19966
+ this._saveTracker.recordChanges(
19967
+ result.changes.activities,
19968
+ result.changes.links,
19969
+ (activityId) => this.coreRuntime.state.getActivity(activityId),
19970
+ (linkId) => this.coreRuntime.state.getLink(linkId)
19971
+ );
19972
+ }
18865
19973
  }
18866
19974
  result = this._withReappliedViewState(action, result);
18867
- if (!result.ok) return result;
19975
+ if (!result.ok) return { result, changedState: false };
18868
19976
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
18869
- this._recordScheduleMutation(options.skipCriticalPath !== true);
19977
+ this._recordScheduleMutation(
19978
+ options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
19979
+ );
19980
+ return { result: toPublicDispatchResult(result), changedState: true };
18870
19981
  }
18871
- return toPublicDispatchResult(result);
19982
+ return { result: toPublicDispatchResult(result), changedState: false };
18872
19983
  }
18873
19984
  recomputeCriticalPath() {
18874
19985
  const operation = this._enqueue(async () => ({
@@ -18878,7 +19989,11 @@ var ScheduleCore = class {
18878
19989
  return operation;
18879
19990
  }
18880
19991
  isCriticalPathSettled() {
18881
- return this._criticalPathRevision === this._scheduleRevision && this._activeCriticalPath === null;
19992
+ return this._criticalPathRevision === this._scheduleRevision && // An admitted mutation has not landed yet, so whatever is applied now is
19993
+ // about to be stale. Without this term `whenCriticalPathSettled` would
19994
+ // spin: it would keep arming calculations that `isCurrent` kills on their
19995
+ // first check, each one paying the project-wide deep copy first.
19996
+ this._pendingScheduleRevision === this._scheduleRevision && this._activeCriticalPath === null;
18882
19997
  }
18883
19998
  async whenCriticalPathSettled() {
18884
19999
  while (!this.isCriticalPathSettled()) {
@@ -18886,7 +20001,11 @@ var ScheduleCore = class {
18886
20001
  }
18887
20002
  }
18888
20003
  _withReappliedViewState(action, result) {
18889
- if (!result.ok || !reappliesViewState(action)) return result;
20004
+ if (!result.ok) return result;
20005
+ if (action.kind === "visibility-set") {
20006
+ this.coreRuntime.state.setFilterProjection(null);
20007
+ }
20008
+ if (!reappliesViewState(action)) return result;
18890
20009
  const merged = this._reapplyViewState(result.changes);
18891
20010
  return merged === null ? result : { ...result, changes: merged };
18892
20011
  }
@@ -18907,11 +20026,44 @@ var ScheduleCore = class {
18907
20026
  */
18908
20027
  _reapplyViewState(changes) {
18909
20028
  const withFilter = this._reapplyActiveFilter(changes);
18910
- const withOrder = this._reapplyActiveOrder(withFilter ?? changes);
18911
- const sequenced = this._emitTouchedBranchOrder(
18912
- withOrder ?? withFilter ?? changes
20029
+ const sequenced = this._emitTouchedBranchOrder(withFilter ?? changes);
20030
+ const withOrder = this._reapplyActiveOrder(
20031
+ sequenced.changes ?? withFilter ?? changes,
20032
+ sequenced.touchedParents
20033
+ );
20034
+ const reapplied = withOrder ?? sequenced.changes ?? withFilter;
20035
+ const finalChanges = reapplied ?? changes;
20036
+ const createdIds = finalChanges.activities.filter((change) => change.kind === "created").map((change) => String(change.id));
20037
+ if (createdIds.length === 0) return reapplied;
20038
+ const createdAndAncestors = new Set(createdIds);
20039
+ const filterProjection = this.coreRuntime.state.getFilterProjection();
20040
+ for (const createdId of createdIds) {
20041
+ if (filterProjection !== null) {
20042
+ filterProjection.pinnedVisibleIds.add(createdId);
20043
+ }
20044
+ let activityId = createdId;
20045
+ while (activityId !== null && activityId !== ROOT_PARENT_ID) {
20046
+ createdAndAncestors.add(activityId);
20047
+ if (filterProjection !== null) {
20048
+ filterProjection.visibleIds.add(activityId);
20049
+ filterProjection.visibleReferenceCounts.set(
20050
+ activityId,
20051
+ (filterProjection.visibleReferenceCounts.get(activityId) ?? 0) + 1
20052
+ );
20053
+ }
20054
+ activityId = this.coreRuntime.state.getParentId(activityId);
20055
+ }
20056
+ }
20057
+ const createdVisibility = applyVisibleSet(
20058
+ this.coreRuntime.state,
20059
+ createdAndAncestors,
20060
+ createdAndAncestors
18913
20061
  );
18914
- return sequenced ?? withOrder ?? withFilter;
20062
+ if (createdVisibility.length === 0) return reapplied;
20063
+ return {
20064
+ ...finalChanges,
20065
+ viewState: [...finalChanges.viewState ?? [], ...createdVisibility]
20066
+ };
18915
20067
  }
18916
20068
  /**
18917
20069
  * Emits `order` for the branches this mutation resequenced, when no user order
@@ -18933,18 +20085,19 @@ var ScheduleCore = class {
18933
20085
  */
18934
20086
  _emitTouchedBranchOrder(changes) {
18935
20087
  const adapter = this.coreRuntime.state;
18936
- if (adapter.getActiveOrder() !== null) return null;
18937
- const touched = collectResequencedParents(
20088
+ const touchedParents = collectResequencedParents(
18938
20089
  changes,
18939
20090
  (activityId) => adapter.getParentId(activityId)
18940
20091
  );
18941
- if (touched.size === 0) return null;
18942
- const order = [...touched].map((parentId) => ({
18943
- parentId,
18944
- childIds: getChildrenInVisualOrder(parentId, adapter)
18945
- })).filter((branch) => branch.childIds.length > 1);
18946
- if (order.length === 0) return null;
18947
- return { ...changes, order };
20092
+ if (adapter.getActiveOrder() !== null) {
20093
+ return { changes: null, touchedParents };
20094
+ }
20095
+ if (touchedParents.size === 0) {
20096
+ return { changes: null, touchedParents };
20097
+ }
20098
+ const order = collectBranchOrderForParents(adapter, touchedParents);
20099
+ if (order.length === 0) return { changes: null, touchedParents };
20100
+ return { changes: { ...changes, order }, touchedParents };
18948
20101
  }
18949
20102
  /**
18950
20103
  * Re-sequences the grid after any mutation that could have changed a value the
@@ -18955,23 +20108,63 @@ var ScheduleCore = class {
18955
20108
  * Production only re-sorts after a bar drag; diverging from that is a
18956
20109
  * deliberate product decision, not an oversight.
18957
20110
  */
18958
- _reapplyActiveOrder(changes) {
20111
+ _reapplyActiveOrder(changes, resequencedParents) {
18959
20112
  const adapter = this.coreRuntime.state;
18960
20113
  if (adapter.getActiveOrder() === null) return null;
18961
20114
  adapter.invalidateDerivedOrder();
18962
- return { ...changes, order: collectBranchOrder(adapter) };
20115
+ const hasIndirectOrderChanges = changes.links.length > 0 || changes.calendars.length > 0;
20116
+ if (hasIndirectOrderChanges) {
20117
+ return { ...changes, order: collectBranchOrder(adapter) };
20118
+ }
20119
+ const affectedParents = new Set(resequencedParents);
20120
+ for (const change of changes.activities) {
20121
+ if (change.kind === "deleted") {
20122
+ return { ...changes, order: collectBranchOrder(adapter) };
20123
+ }
20124
+ const parentId = adapter.getParentId(change.id);
20125
+ affectedParents.add(
20126
+ parentId === null ? ROOT_PARENT_ID : String(parentId)
20127
+ );
20128
+ }
20129
+ const order = collectBranchOrderForParents(adapter, affectedParents);
20130
+ if (order.length === 0) return null;
20131
+ return { ...changes, order };
18963
20132
  }
18964
20133
  _reapplyActiveFilter(changes) {
18965
- const filter = this.coreRuntime.state.getActiveFilter();
18966
- if (filter === null) return null;
18967
20134
  const adapter = this.coreRuntime.state;
18968
- const visibleIds = evaluateVisibleIds({
18969
- activities: adapter.getAllActivities(),
18970
- parentOf: (activityId) => adapter.getParentId(activityId),
20135
+ const filter = adapter.getActiveFilter();
20136
+ if (filter === null) return null;
20137
+ const context = buildFilterContext(this.coreRuntime.sector.hoursPerDay);
20138
+ const projection = adapter.getFilterProjection();
20139
+ const projectionIsCurrent = isCurrentFilterProjection(
20140
+ projection,
18971
20141
  filter,
18972
- context: buildFilterContext(this.coreRuntime.sector.hoursPerDay)
18973
- });
18974
- const viewState = applyVisibleSet(adapter, visibleIds);
20142
+ context
20143
+ );
20144
+ const changedActivities = changesRequireFilterProjectionRebuild(changes) ? null : collectChangedActivities(changes, adapter);
20145
+ let affectedIds;
20146
+ let visibleIds;
20147
+ const needsProjectionRebuild = changedActivities === null || !projectionIsCurrent;
20148
+ if (needsProjectionRebuild) {
20149
+ const rebuiltProjection = buildFilterProjection({
20150
+ activities: adapter.getAllActivities(),
20151
+ parentOf: (activityId) => adapter.getParentId(activityId),
20152
+ filter,
20153
+ context,
20154
+ ...projectionIsCurrent ? { pinnedVisibleIds: projection.pinnedVisibleIds } : {}
20155
+ });
20156
+ adapter.setFilterProjection(rebuiltProjection);
20157
+ visibleIds = rebuiltProjection.visibleIds;
20158
+ affectedIds = adapter.getAllIds();
20159
+ } else {
20160
+ affectedIds = updateFilterProjection(
20161
+ projection,
20162
+ changedActivities,
20163
+ (activityId) => adapter.getParentId(activityId)
20164
+ );
20165
+ visibleIds = projection.visibleIds;
20166
+ }
20167
+ const viewState = applyVisibleSet(adapter, visibleIds, affectedIds);
18975
20168
  if (viewState.length === 0) return null;
18976
20169
  return {
18977
20170
  ...changes,
@@ -18980,6 +20173,10 @@ var ScheduleCore = class {
18980
20173
  }
18981
20174
  _recordScheduleMutation(criticalPathIsFresh) {
18982
20175
  this._scheduleRevision++;
20176
+ this._pendingScheduleRevision = Math.max(
20177
+ this._pendingScheduleRevision,
20178
+ this._scheduleRevision
20179
+ );
18983
20180
  if (this._activeCriticalPath) {
18984
20181
  this._activeCriticalPath.cancellation.cancelled = true;
18985
20182
  }
@@ -19010,23 +20207,30 @@ var ScheduleCore = class {
19010
20207
  this.coreRuntime.baseCalendars
19011
20208
  )
19012
20209
  );
19013
- const isCurrent = () => !cancellation.cancelled && this._status !== SCHEDULE_CORE_STATUS.DESTROYED && this._scheduleRevision === revision;
20210
+ const isCurrent = () => !cancellation.cancelled && this._status !== SCHEDULE_CORE_STATUS.DESTROYED && this._scheduleRevision === revision && // A mutation admitted but not yet settled is enough to make this result
20211
+ // garbage. Without this term the calculation only learns it is obsolete
20212
+ // when the mutation finishes, which measured live is the same instant the
20213
+ // job itself ends.
20214
+ this._pendingScheduleRevision === this._scheduleRevision;
19014
20215
  const promise = (async () => {
19015
20216
  const calculatedFields = await computeCriticalPathFields(
19016
20217
  isolatedState,
19017
20218
  this.coreRuntime.sector.hoursPerDay,
19018
20219
  isCurrent
19019
20220
  );
19020
- if (!isCurrent()) return null;
19021
- const fieldsByActivity = calculatedFields ?? /* @__PURE__ */ new Map();
20221
+ if (!isCurrent() || calculatedFields === null) return null;
19022
20222
  return this._enqueue(async () => {
19023
20223
  if (!isCurrent()) return null;
19024
20224
  this.coreRuntime.state.beginWriteCapture();
19025
20225
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
19026
20226
  let changes;
19027
20227
  try {
19028
- for (const [activityId, fields] of fieldsByActivity) {
19029
- this.coreRuntime.state.setActivityFields(activityId, fields);
20228
+ for (const [activityId, fields] of calculatedFields) {
20229
+ setCriticalPathFieldsIfChanged(
20230
+ this.coreRuntime.state,
20231
+ activityId,
20232
+ fields
20233
+ );
19030
20234
  }
19031
20235
  changes = await assembleChangeSet(this.coreRuntime.state, {
19032
20236
  source: { kind: "init" },
@@ -19088,47 +20292,17 @@ var ScheduleCore = class {
19088
20292
  }
19089
20293
  return { changes };
19090
20294
  }
19091
- _recomputeAfterHistoryRestore() {
19092
- const state = this.coreRuntime.state;
19093
- recomputeAllProgressRollup(state);
19094
- recomputeCanonicalRealWork(state);
19095
- emitRealCost(state);
19096
- if (this.coreRuntime.clock) {
19097
- const now = endOfLocalDay(this.coreRuntime.clock());
19098
- applyExpectedProgressLive(state, now);
19099
- let hasActiveBaseline2 = false;
19100
- state.forEachActivity((activity) => {
19101
- hasActiveBaseline2 ||= getActiveBaseline(activity) !== null;
19102
- });
19103
- if (hasActiveBaseline2) {
19104
- runExpectedProgressBase(
19105
- state,
19106
- now,
19107
- this.coreRuntime.baseCalendars.find(
19108
- (calendar) => calendar.baseDefault
19109
- )?.id ?? null
19110
- );
19111
- }
19112
- applyStatusPass(state, this.coreRuntime.sector.statusCriteria);
19113
- }
19114
- }
19115
20295
  undo(options = {}) {
20296
+ const admitted = this._admitScheduleMutation();
20297
+ let changedState = false;
19116
20298
  const operation = this._enqueue(async () => {
19117
- this.assertReady();
19118
- const entry = this._undo.takeUndo();
19119
- if (!entry) return null;
19120
- applyUndo(this.coreRuntime.state, entry);
19121
- this._recomputeAfterHistoryRestore();
19122
- this._recordScheduleMutation(false);
19123
- if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
19124
- this._undo.pushRedo(entry);
19125
- this._undo.resetCoalesce();
19126
- const changes = buildInverseChangeSet(
19127
- this.coreRuntime.state,
19128
- entry,
19129
- "before"
19130
- );
19131
- return this._reapplyViewState(changes) ?? changes;
20299
+ try {
20300
+ return await this._runUndo(options, (changed) => {
20301
+ changedState = changed;
20302
+ });
20303
+ } finally {
20304
+ this._settleScheduleMutation(admitted, changedState);
20305
+ }
19132
20306
  });
19133
20307
  void operation.then(
19134
20308
  (changes) => {
@@ -19145,23 +20319,45 @@ var ScheduleCore = class {
19145
20319
  );
19146
20320
  return operation;
19147
20321
  }
19148
- redo(options = {}) {
19149
- const operation = this._enqueue(async () => {
20322
+ async _runUndo(options, markChanged) {
20323
+ {
19150
20324
  this.assertReady();
19151
- const entry = this._undo.takeRedo();
20325
+ if (options.expectedUndoDepth !== void 0 && this._undo.undoDepth() !== options.expectedUndoDepth) {
20326
+ return null;
20327
+ }
20328
+ const entry = this._undo.takeUndo();
19152
20329
  if (!entry) return null;
19153
- applyRedo(this.coreRuntime.state, entry);
19154
- this._recomputeAfterHistoryRestore();
20330
+ applyUndo(this.coreRuntime.state, entry);
19155
20331
  this._recordScheduleMutation(false);
20332
+ markChanged(true);
19156
20333
  if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
19157
- this._undo.pushUndo(entry);
20334
+ this._undo.pushRedo(entry);
19158
20335
  this._undo.resetCoalesce();
19159
20336
  const changes = buildInverseChangeSet(
19160
20337
  this.coreRuntime.state,
19161
20338
  entry,
19162
- "after"
20339
+ "before"
20340
+ );
20341
+ this._saveTracker.recordChanges(
20342
+ changes.activities,
20343
+ changes.links,
20344
+ (activityId) => this.coreRuntime.state.getActivity(activityId),
20345
+ (linkId) => this.coreRuntime.state.getLink(linkId)
19163
20346
  );
19164
20347
  return this._reapplyViewState(changes) ?? changes;
20348
+ }
20349
+ }
20350
+ redo(options = {}) {
20351
+ const admitted = this._admitScheduleMutation();
20352
+ let changedState = false;
20353
+ const operation = this._enqueue(async () => {
20354
+ try {
20355
+ return await this._runRedo((changed) => {
20356
+ changedState = changed;
20357
+ });
20358
+ } finally {
20359
+ this._settleScheduleMutation(admitted, changedState);
20360
+ }
19165
20361
  });
19166
20362
  void operation.then(
19167
20363
  (changes) => {
@@ -19178,6 +20374,29 @@ var ScheduleCore = class {
19178
20374
  );
19179
20375
  return operation;
19180
20376
  }
20377
+ async _runRedo(markChanged) {
20378
+ this.assertReady();
20379
+ const entry = this._undo.takeRedo();
20380
+ if (!entry) return null;
20381
+ applyRedo(this.coreRuntime.state, entry);
20382
+ this._recordScheduleMutation(false);
20383
+ markChanged(true);
20384
+ if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
20385
+ this._undo.pushUndo(entry);
20386
+ this._undo.resetCoalesce();
20387
+ const changes = buildInverseChangeSet(
20388
+ this.coreRuntime.state,
20389
+ entry,
20390
+ "after"
20391
+ );
20392
+ this._saveTracker.recordChanges(
20393
+ changes.activities,
20394
+ changes.links,
20395
+ (activityId) => this.coreRuntime.state.getActivity(activityId),
20396
+ (linkId) => this.coreRuntime.state.getLink(linkId)
20397
+ );
20398
+ return this._reapplyViewState(changes) ?? changes;
20399
+ }
19181
20400
  canUndo() {
19182
20401
  return this._undo.canUndo();
19183
20402
  }
@@ -19218,10 +20437,17 @@ var ScheduleCore = class {
19218
20437
  );
19219
20438
  }
19220
20439
  }
19221
- _rollback() {
20440
+ _rollback(generatorSnapshot) {
19222
20441
  try {
19223
20442
  this.coreRuntime.state.restoreFromCapture();
19224
20443
  this.coreRuntime.customIdTracker.rollbackCustomIdTransaction();
20444
+ if (generatorSnapshot) {
20445
+ this.coreRuntime.activityIdGenerator.restore(generatorSnapshot[0]);
20446
+ this.coreRuntime.linkIdGenerator.restore(generatorSnapshot[1]);
20447
+ this.coreRuntime.uniqueCorrelativeIdGenerator.restore(
20448
+ generatorSnapshot[2]
20449
+ );
20450
+ }
19225
20451
  } catch (restoreError) {
19226
20452
  this._status = SCHEDULE_CORE_STATUS.POISONED;
19227
20453
  this.coreRuntime.reporter.error(
@@ -19241,18 +20467,6 @@ function toPublicDispatchResult(result) {
19241
20467
  return publicResult;
19242
20468
  }
19243
20469
 
19244
- // src/boundary/save/link-changes.ts
19245
- function checkNoUpdatedLinks(baseline, current) {
19246
- const baselineById = new Map(
19247
- baseline.map((link) => [String(link.id), link])
19248
- );
19249
- return current.filter((link) => {
19250
- const base = baselineById.get(String(link.id));
19251
- if (!base) return false;
19252
- return base.lag !== link.lag || base.type !== link.type;
19253
- });
19254
- }
19255
-
19256
20470
  // src/boundary/save/unsaved.ts
19257
20471
  var getUnsavedActivities = (activities = []) => (activities ?? []).filter((activity) => !activity.proplannerId);
19258
20472