@outbuild-company/schedule-core 1.6.1 → 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,
@@ -10488,9 +10778,11 @@ async function createActivityCore(action, deps, opts = {}) {
10488
10778
  if (parentCustomId) {
10489
10779
  deps.customIdTracker.releaseCustomId(parentCustomId);
10490
10780
  }
10491
- recomputeHhCascadeForActivity(newId, adapter);
10492
- recomputeUsedCostCascadeForActivity(newId, adapter);
10493
- recomputeCostCascadeForActivity(newId, adapter);
10781
+ if (!opts.skipRollupCascades) {
10782
+ recomputeHhCascadeForActivity(newId, adapter);
10783
+ recomputeUsedCostCascadeForActivity(newId, adapter);
10784
+ recomputeCostCascadeForActivity(newId, adapter);
10785
+ }
10494
10786
  recomputeAndFoldCorrelatives(
10495
10787
  adapter,
10496
10788
  newId,
@@ -10662,331 +10954,97 @@ function findDefaultCalendarId(calendars) {
10662
10954
  return "global";
10663
10955
  }
10664
10956
 
10665
- // src/dispatch/handlers/activity-paste.ts
10666
- function resolvePasteRootDestination(destination, adapter) {
10667
- if ("afterSiblingId" in destination) {
10668
- const sibling = adapter.getActivity(destination.afterSiblingId);
10669
- if (!sibling) {
10670
- 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);
10671
10973
  }
10672
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
10673
- return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
10674
10974
  }
10675
- const parentId = destination.parentId;
10676
- const siblings = collectSiblings(adapter, parentId).sort(
10677
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
10678
- );
10679
- const before = siblings[destination.index]?.id;
10680
- 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;
10681
10987
  }
10682
- function applyPasteDomainSemantics(overrides) {
10683
- 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;
10684
11004
  return {
10685
- ...content,
10686
- progress: 0,
10687
- ponderator: 0,
10688
- usedCost: 0,
10689
- workHours: 0,
10690
- 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
10691
11013
  };
10692
11014
  }
10693
- async function dispatchActivityPaste(action, options, deps) {
10694
- const { adapter, scheduler, sector } = deps;
10695
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
10696
- if (!rootDest.ok) return rootDest;
10697
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
10698
- const touched = /* @__PURE__ */ new Set();
10699
- const originalToNew = /* @__PURE__ */ new Map();
10700
- const createdIds = [];
10701
- let prevRootId;
10702
- for (const input of action.activities) {
10703
- const mappedParent = originalToNew.get(input.originalParentId);
10704
- let parentId;
10705
- let afterSiblingId;
10706
- let beforeSiblingId;
10707
- if (mappedParent !== void 0) {
10708
- parentId = mappedParent;
10709
- } else {
10710
- parentId = rootDest.parentId;
10711
- if (prevRootId === void 0) {
10712
- afterSiblingId = rootDest.firstAfter;
10713
- beforeSiblingId = rootDest.firstBefore;
10714
- } else {
10715
- afterSiblingId = prevRootId;
10716
- }
10717
- }
10718
- const core = await createActivityCore(
10719
- {
10720
- parentId,
10721
- afterSiblingId,
10722
- beforeSiblingId,
10723
- ...input.activityId ? { activityId: input.activityId } : {},
10724
- overrides: applyPasteDomainSemantics(input.overrides),
10725
- eventSource: action.eventSource
10726
- },
10727
- deps,
10728
- {
10729
- skipCorrelativeRecompute: true,
10730
- skipBeforeSnap: true,
10731
- customIdReferenceId: action.referenceActivityId
10732
- }
10733
- );
10734
- if (!core.ok) return core;
10735
- if (input.baselineSnapshot !== void 0) {
10736
- const snapshot = input.baselineSnapshot;
10737
- adapter.setActivityField(
10738
- core.newId,
10739
- "baselineSnapshot",
10740
- snapshot ? {
10741
- ...snapshot,
10742
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
10743
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
10744
- } : null
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))
10745
11044
  );
10746
- }
10747
- originalToNew.set(input.originalId, core.newId);
10748
- createdIds.push(core.newId);
10749
- if (mappedParent === void 0) prevRootId = core.newId;
10750
- }
10751
- const correlativeShifts = recomputeCorrelativeIds(adapter);
10752
- const createdIdSet = new Set(createdIds.map(String));
10753
- foldCorrelativeShifts(
10754
- adapter,
10755
- correlativeShifts,
10756
- createdIdSet,
10757
- beforeSnap,
10758
- touched
10759
- );
10760
- for (const createdId of createdIds) touched.add(String(createdId));
10761
- const appliedLinks = [];
10762
- for (const link of action.links) {
10763
- const newSource = originalToNew.get(link.source);
10764
- const newTarget = originalToNew.get(link.target);
10765
- if (newSource === void 0 || newTarget === void 0) continue;
10766
- const op = {
10767
- kind: "create",
10768
- source: newSource,
10769
- target: newTarget,
10770
- type: link.type,
10771
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
10772
- };
10773
- const res = applyLinkOperation(op, {
10774
- port: adapter,
10775
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
10776
- });
10777
- appliedLinks.push({
10778
- op,
10779
- finalLinkId: res.applied ? res.linkId : null,
10780
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
10781
- });
10782
- }
10783
- const { scheduledIds } = await runPostMutation(
10784
- {
10785
- adapter,
10786
- scheduler,
10787
- sector: deps.sector,
10788
- defaultBaseCalendarId: deps.defaultBaseCalendarId
10789
- },
10790
- {
10791
- action,
10792
- autoscheduleFrom: createdIds[0] ?? null,
10793
- recomputeParentsFrom: createdIds,
10794
- now: deps.now,
10795
- options
10796
- }
10797
- );
10798
- const linkChanges = buildLinkChangesForBatch(
10799
- appliedLinks,
10800
- /* @__PURE__ */ new Map(),
10801
- adapter
10802
- );
10803
- return {
10804
- ok: true,
10805
- changes: await assembleChangeSet(adapter, {
10806
- source: action,
10807
- beforeSnap,
10808
- touchedIds: touched,
10809
- scheduledIds,
10810
- links: linkChanges,
10811
- hoursPerDay: sector.hoursPerDay
10812
- })
10813
- };
10814
- }
10815
-
10816
- // src/dispatch/handlers/sir-sync.ts
10817
- function dispatchSirSync(action, deps) {
10818
- const { adapter } = deps;
10819
- const id = action.activityId;
10820
- const snapshot = adapter.getActivity(id);
10821
- if (!snapshot) {
10822
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
10823
- }
10824
- const before = snapshot.pendingRequestIds ?? [];
10825
- const after = action.pendingRequests.map((r) => r.id);
10826
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
10827
- const change = {
10828
- id,
10829
- kind: "updated",
10830
- fields: { pendingRequestIds: { before, after } },
10831
- after: { id, pendingRequestIds: after }
10832
- };
10833
- return {
10834
- ok: true,
10835
- changes: {
10836
- source: action,
10837
- activities: [change],
10838
- links: [],
10839
- calendars: [],
10840
- trackingEvents: []
10841
- }
10842
- };
10843
- }
10844
-
10845
- // src/dispatch/handlers/activity-lookahead-sync.ts
10846
- function dispatchActivityLookaheadSync(action, deps) {
10847
- const ids = [...new Set(action.activityIds.map(String))];
10848
- const snapshots = ids.map((id) => deps.adapter.getActivity(id));
10849
- if (snapshots.some((snapshot) => !snapshot)) {
10850
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
10851
- }
10852
- const activities = [];
10853
- for (const [index, id] of ids.entries()) {
10854
- const snapshot = snapshots[index];
10855
- if (!snapshot) continue;
10856
- const fields = {};
10857
- if (snapshot.isLookahead !== action.isLookahead) {
10858
- fields.isLookahead = {
10859
- before: snapshot.isLookahead,
10860
- after: action.isLookahead
10861
- };
10862
- deps.adapter.setActivityField(id, "isLookahead", action.isLookahead);
10863
- }
10864
- if (snapshot.hasLookaheadTasks !== action.hasLookaheadTasks) {
10865
- fields.hasLookaheadTasks = {
10866
- before: snapshot.hasLookaheadTasks,
10867
- after: action.hasLookaheadTasks
10868
- };
10869
- deps.adapter.setActivityField(
10870
- id,
10871
- "hasLookaheadTasks",
10872
- action.hasLookaheadTasks
10873
- );
10874
- }
10875
- if (Object.keys(fields).length === 0) continue;
10876
- activities.push({
10877
- id,
10878
- kind: "updated",
10879
- fields,
10880
- after: {
10881
- ...snapshot,
10882
- isLookahead: action.isLookahead,
10883
- hasLookaheadTasks: action.hasLookaheadTasks
10884
- }
10885
- });
10886
- }
10887
- return {
10888
- ok: true,
10889
- changes: {
10890
- source: action,
10891
- activities,
10892
- links: [],
10893
- calendars: [],
10894
- trackingEvents: []
10895
- }
10896
- };
10897
- }
10898
-
10899
- // src/propagations/upward/recompute-rollup-cascades.ts
10900
- function recomputeRollupCascadesForParent(parentId, adapter) {
10901
- recomputeHhCascadeForParent(parentId, adapter);
10902
- recomputeUsedCostCascadeForParent(parentId, adapter);
10903
- recomputeCostCascadeForParent(parentId, adapter);
10904
- }
10905
-
10906
- // src/propagations/shared/traversal-helpers.ts
10907
- function collectDescendantIds(rootIds, adapter) {
10908
- const out = /* @__PURE__ */ new Set();
10909
- const stack = [];
10910
- for (const id of rootIds) {
10911
- const key = String(id);
10912
- if (!out.has(key)) {
10913
- out.add(key);
10914
- stack.push(key);
10915
- }
10916
- }
10917
- while (stack.length > 0) {
10918
- const current = stack.pop();
10919
- const children = adapter.getChildren(current);
10920
- for (const childId of children) {
10921
- const key = String(childId);
10922
- if (!out.has(key)) {
10923
- out.add(key);
10924
- stack.push(key);
10925
- }
10926
- }
10927
- }
10928
- return out;
10929
- }
10930
- function collectIncidentLinkIds(activityIds, adapter) {
10931
- const out = /* @__PURE__ */ new Set();
10932
- for (const activityId of activityIds) {
10933
- for (const linkId of adapter.getOutgoingLinkIds(activityId)) {
10934
- out.add(String(linkId));
10935
- }
10936
- for (const linkId of adapter.getIncomingLinkIds(activityId)) {
10937
- out.add(String(linkId));
10938
- }
10939
- }
10940
- return [...out];
10941
- }
10942
-
10943
- // src/internal/hierarchy/parent-demotion.ts
10944
- function restoredPromotionFields(restore) {
10945
- if (!restore) return null;
10946
- return {
10947
- type: restore.type,
10948
- startDate: new Date(restore.startDate),
10949
- endDate: new Date(restore.endDate),
10950
- durationHours: restore.durationHours,
10951
- expectedProgressBaseline: restore.expectedProgressBaseline,
10952
- constraintType: restore.constraintType,
10953
- constraintDate: restore.constraintDate ? new Date(restore.constraintDate) : null,
10954
- progress: restore.progress
10955
- };
10956
- }
10957
- function canonicalEndDateField(parent, durationHours, computeEndDate) {
10958
- const startDate = parent.startDate;
10959
- if (!computeEndDate || !(startDate instanceof Date)) return {};
10960
- return { endDate: computeEndDate(startDate, durationHours) };
10961
- }
10962
- function buildParentDemotionMutations(input) {
10963
- if (input.remainingChildIds.length > 0) return null;
10964
- const parent = input.parent;
10965
- const canonicalDuration = input.defaultDurationHours;
10966
- const hasCanonicalDefaults = Number.isFinite(canonicalDuration);
10967
- const fields = restoredPromotionFields(input.promotionSnapshot) ?? (hasCanonicalDefaults ? {
10968
- type: DEMOTION_TARGET_TYPE,
10969
- durationHours: canonicalDuration,
10970
- progress: 0,
10971
- autoScheduling: true,
10972
- constraintType: "asap",
10973
- constraintDate: null,
10974
- hasNewActivities: false,
10975
- ...canonicalEndDateField(
10976
- parent,
10977
- canonicalDuration,
10978
- input.computeEndDate
10979
- )
10980
- } : { type: DEMOTION_TARGET_TYPE });
10981
- if (input.idsRemoved && input.idsRemoved.size > 0) {
10982
- const existing = Array.isArray(parent.newActivityIds) ? parent.newActivityIds : [];
10983
- if (existing.length > 0) {
10984
- const filtered = existing.filter(
10985
- (id) => !input.idsRemoved.has(id) && !input.idsRemoved.has(String(id))
10986
- );
10987
- if (filtered.length !== existing.length) {
10988
- fields.newActivityIds = filtered;
10989
- }
11045
+ if (filtered.length !== existing.length) {
11046
+ fields.newActivityIds = filtered;
11047
+ }
10990
11048
  }
10991
11049
  }
10992
11050
  if (parent.hasLookaheadTasks) fields.isLookahead = true;
@@ -11194,7 +11252,647 @@ function buildLinkDeletions(beforeLinks) {
11194
11252
  after: null
11195
11253
  });
11196
11254
  }
11197
- 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
+ };
11198
11896
  }
11199
11897
 
11200
11898
  // src/internal/hierarchy/invalid-links.ts
@@ -12792,7 +13490,10 @@ async function dispatchStatusCriteriaSet(action, deps) {
12792
13490
  }
12793
13491
 
12794
13492
  // src/dispatch/dispatch.ts
12795
- 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
+ }
12796
13497
  if (action.kind === "persistence-acknowledge") {
12797
13498
  return dispatchPersistenceAcknowledge(action, deps);
12798
13499
  }
@@ -16026,6 +16727,7 @@ var ScheduleState = class {
16026
16727
  };
16027
16728
  _writeCapture = new WriteCapture();
16028
16729
  _viewState = new ViewStateStore();
16730
+ _filterProjection = null;
16029
16731
  _viewStateBefore = null;
16030
16732
  _lastStartDateBefore = null;
16031
16733
  _promotionSnapshotBefore = null;
@@ -16140,6 +16842,13 @@ var ScheduleState = class {
16140
16842
  setActiveFilter(nextFilter) {
16141
16843
  this._captureViewStateOnce();
16142
16844
  this._viewState.setActiveFilter(nextFilter);
16845
+ this._filterProjection = null;
16846
+ }
16847
+ getFilterProjection() {
16848
+ return this._filterProjection;
16849
+ }
16850
+ setFilterProjection(projection) {
16851
+ this._filterProjection = projection;
16143
16852
  }
16144
16853
  getActiveOrder() {
16145
16854
  return this._viewState.getActiveOrder();
@@ -16181,6 +16890,7 @@ var ScheduleState = class {
16181
16890
  }
16182
16891
  hydrateViewState(seed2) {
16183
16892
  this._viewState.hydrate(seed2);
16893
+ this._filterProjection = null;
16184
16894
  this.invalidateDerivedOrder();
16185
16895
  }
16186
16896
  _captureViewStateOnce() {
@@ -16381,6 +17091,7 @@ var ScheduleState = class {
16381
17091
  this._restoreActivityFields(before);
16382
17092
  if (this._viewStateBefore !== null) {
16383
17093
  this._viewState.restore(this._viewStateBefore);
17094
+ this._filterProjection = null;
16384
17095
  this.invalidateDerivedOrder();
16385
17096
  }
16386
17097
  if (this._promotionSnapshotBefore !== null) {
@@ -16590,8 +17301,10 @@ function buildHistoryChangeSet(changes) {
16590
17301
  ...entry,
16591
17302
  after: null
16592
17303
  }));
17304
+ const source = changes.source.kind === "activity-batch" ? { ...changes.source, items: [], links: [] } : changes.source;
16593
17305
  return cloneDomainValue({
16594
17306
  ...changes,
17307
+ source,
16595
17308
  activities: activitiesWithoutAfter,
16596
17309
  links: linksWithoutAfter
16597
17310
  });
@@ -16608,7 +17321,7 @@ function captureCreatedSnapshots(state, changeSet) {
16608
17321
  if (live) afterSnap.set(String(ch.id), structuredCloneActivity(live));
16609
17322
  }
16610
17323
  for (const ch of changeSet.links) {
16611
- if (ch.kind !== "created") continue;
17324
+ if (ch.kind === "deleted") continue;
16612
17325
  const live = state.getLink(String(ch.id));
16613
17326
  if (live) afterLinks.set(String(ch.id), { ...live });
16614
17327
  }
@@ -16831,6 +17544,17 @@ function applyChangeSetSide(state, entry, side) {
16831
17544
  for (const ch of cs.links) {
16832
17545
  if (ch.kind === "updated") {
16833
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
+ }
16834
17558
  for (const [field, fv] of Object.entries(ch.fields)) {
16835
17559
  setLinkFieldDynamic(
16836
17560
  state,
@@ -17101,9 +17825,26 @@ function idArrayEquals(current, saved) {
17101
17825
  return currentIds.every((value, index) => value === savedIds[index]);
17102
17826
  }
17103
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
+
17104
17843
  // src/boundary/save/save-tracker.ts
17105
17844
  var SaveTracker = class {
17106
17845
  initialized = false;
17846
+ dirtyActivityIds = /* @__PURE__ */ new Set();
17847
+ dirtyLinkIds = /* @__PURE__ */ new Set();
17107
17848
  linksAtLastSave = /* @__PURE__ */ new Map();
17108
17849
  activitiesAtLastSave = /* @__PURE__ */ new Map();
17109
17850
  init(activities, links) {
@@ -17113,10 +17854,16 @@ var SaveTracker = class {
17113
17854
  }
17114
17855
  snapshotActivities(activities) {
17115
17856
  this.activitiesAtLastSave.clear();
17857
+ this.dirtyActivityIds.clear();
17116
17858
  for (const activity of activities) {
17117
- 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);
17118
17865
  this.activitiesAtLastSave.set(String(activity.id), {
17119
- persistable: projectPersistableActivity(activity),
17866
+ persistable,
17120
17867
  deletedSnapshot: {
17121
17868
  id: activity.id,
17122
17869
  proplannerId: activity.proplannerId,
@@ -17124,12 +17871,20 @@ var SaveTracker = class {
17124
17871
  hadBaseline: activity.baselineSnapshot !== null || activity.baselinePoints.length > 0
17125
17872
  }
17126
17873
  });
17874
+ if (activityChangedSince(activity, persistable)) {
17875
+ this.dirtyActivityIds.add(activityId);
17876
+ }
17127
17877
  }
17128
17878
  }
17129
17879
  snapshotLinks(links) {
17130
17880
  this.linksAtLastSave.clear();
17881
+ this.dirtyLinkIds.clear();
17131
17882
  for (const link of links) {
17132
- if (link.proplannerId == null) continue;
17883
+ const linkId = String(link.id);
17884
+ if (link.proplannerId == null) {
17885
+ this.dirtyLinkIds.add(linkId);
17886
+ continue;
17887
+ }
17133
17888
  this.linksAtLastSave.set(String(link.id), {
17134
17889
  id: link.id,
17135
17890
  proplannerId: link.proplannerId,
@@ -17140,21 +17895,52 @@ var SaveTracker = class {
17140
17895
  });
17141
17896
  }
17142
17897
  }
17143
- hasUnsavedChanges(activities, links) {
17144
- if (!this.initialized) return false;
17145
- if (this.deletedActivities(activities).length > 0) return true;
17146
- if (this.deletedLinks(links).length > 0) return true;
17147
- const activityChanged = activities.some((activity) => {
17148
- if (activity.proplannerId == null) return true;
17149
- const saved = this.activitiesAtLastSave.get(String(activity.id));
17150
- return !saved || activityChangedSince(activity, saved.persistable);
17151
- });
17152
- if (activityChanged) return true;
17153
- return links.some((link) => {
17154
- if (link.proplannerId == null) return true;
17155
- const saved = this.linksAtLastSave.get(String(link.id));
17156
- return !saved || link.lag !== saved.lag || link.type !== saved.type;
17157
- });
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);
17158
17944
  }
17159
17945
  deletedActivities(activities) {
17160
17946
  if (!this.initialized) return [];
@@ -17192,10 +17978,13 @@ var SaveTracker = class {
17192
17978
  if (link.proplannerId == null) return false;
17193
17979
  const saved = this.linksAtLastSave.get(String(link.id));
17194
17980
  if (!saved) return true;
17195
- return link.lag !== saved.lag || link.type !== saved.type;
17981
+ return linkChangedSince(saved, link);
17196
17982
  });
17197
17983
  }
17198
17984
  clear() {
17985
+ this.initialized = false;
17986
+ this.dirtyActivityIds.clear();
17987
+ this.dirtyLinkIds.clear();
17199
17988
  this.linksAtLastSave.clear();
17200
17989
  this.activitiesAtLastSave.clear();
17201
17990
  }
@@ -17797,6 +18586,12 @@ function createActivityIdGenerator(options = {}) {
17797
18586
  const n = typeof id === "number" ? id : Number(id);
17798
18587
  if (!Number.isFinite(n)) return;
17799
18588
  raise(n);
18589
+ },
18590
+ snapshot() {
18591
+ return counter;
18592
+ },
18593
+ restore(snapshot) {
18594
+ counter = snapshot;
17800
18595
  }
17801
18596
  };
17802
18597
  }
@@ -17822,6 +18617,12 @@ function createLinkIdGenerator(options = {}) {
17822
18617
  const n = typeof id === "number" ? id : Number(id);
17823
18618
  if (!Number.isFinite(n)) return;
17824
18619
  raise(n);
18620
+ },
18621
+ snapshot() {
18622
+ return counter;
18623
+ },
18624
+ restore(snapshot) {
18625
+ counter = snapshot;
17825
18626
  }
17826
18627
  };
17827
18628
  }
@@ -17851,6 +18652,12 @@ function createUniqueCorrelativeIdGenerator(options = {}) {
17851
18652
  if (uid2 == null) return;
17852
18653
  const n = typeof uid2 === "number" ? uid2 : Number(uid2);
17853
18654
  raise(n);
18655
+ },
18656
+ snapshot() {
18657
+ return counter;
18658
+ },
18659
+ restore(snapshot) {
18660
+ counter = snapshot;
17854
18661
  }
17855
18662
  };
17856
18663
  }
@@ -17927,13 +18734,13 @@ function minSuffixFromArray(suffixes) {
17927
18734
  if (!suffixes || suffixes.length === 0) {
17928
18735
  throw new Error("minSuffixFromArray requires non-empty array");
17929
18736
  }
17930
- return suffixes.reduce((min, current) => minSuffix(min, current));
18737
+ return suffixes.reduce((min2, current) => minSuffix(min2, current));
17931
18738
  }
17932
18739
  function maxSuffixFromArray(suffixes) {
17933
18740
  if (!suffixes || suffixes.length === 0) {
17934
18741
  throw new Error("maxSuffixFromArray requires non-empty array");
17935
18742
  }
17936
- return suffixes.reduce((max, current) => maxSuffix(max, current));
18743
+ return suffixes.reduce((max2, current) => maxSuffix(max2, current));
17937
18744
  }
17938
18745
 
17939
18746
  // src/generators/custom-id/generation-strategies.ts
@@ -18627,13 +19434,204 @@ function initializeCore(input) {
18627
19434
  };
18628
19435
  }
18629
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
+
18630
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
+ }
18631
19605
  var ScheduleCore = class {
18632
19606
  _status = SCHEDULE_CORE_STATUS.READY;
18633
19607
  coreRuntime;
18634
19608
  _undo = new UndoRecorder();
18635
19609
  _opQueue = Promise.resolve();
18636
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;
18637
19635
  _criticalPathRevision = -1;
18638
19636
  _activeCriticalPath = null;
18639
19637
  constructor(input) {
@@ -18686,6 +19684,16 @@ var ScheduleCore = class {
18686
19684
  this.assertReady();
18687
19685
  return readActivity(this.coreRuntime.state, id);
18688
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
+ }
18689
19697
  getAllActivitiesView() {
18690
19698
  this.assertReady();
18691
19699
  return readAllActivities(this.coreRuntime.state);
@@ -18783,10 +19791,7 @@ var ScheduleCore = class {
18783
19791
  }
18784
19792
  hasUnsavedChanges() {
18785
19793
  this.assertReady();
18786
- return this._saveTracker.hasUnsavedChanges(
18787
- this.coreRuntime.state.getAllActivities(),
18788
- this.getAllLinksView()
18789
- );
19794
+ return this._saveTracker.hasUnsavedChanges();
18790
19795
  }
18791
19796
  getDeletedActivitiesSinceLastSave() {
18792
19797
  this.assertReady();
@@ -18804,10 +19809,90 @@ var ScheduleCore = class {
18804
19809
  );
18805
19810
  return workPromise;
18806
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
+ }
18807
19877
  async dispatch(action, options = {}) {
18808
- 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
+ }
18809
19894
  }
18810
- async _dispatchInner(action, options = {}) {
19895
+ async _runDispatch(action, options) {
18811
19896
  this.assertReady();
18812
19897
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
18813
19898
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
@@ -18815,11 +19900,16 @@ var ScheduleCore = class {
18815
19900
  inputUnit: options.inputUnit ?? "days"
18816
19901
  });
18817
19902
  const historyPolicy = getDispatchHistoryPolicy(action);
19903
+ const generatorSnapshot = [
19904
+ this.coreRuntime.activityIdGenerator.snapshot(),
19905
+ this.coreRuntime.linkIdGenerator.snapshot(),
19906
+ this.coreRuntime.uniqueCorrelativeIdGenerator.snapshot()
19907
+ ];
18818
19908
  this.coreRuntime.state.beginWriteCapture();
18819
19909
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
18820
19910
  let result;
18821
19911
  try {
18822
- result = await dispatch(action, options, {
19912
+ result = await performDispatch(action, options, {
18823
19913
  adapter: this.coreRuntime.state,
18824
19914
  ctx: pipelineContext,
18825
19915
  scheduler: this.coreRuntime.scheduler,
@@ -18835,9 +19925,9 @@ var ScheduleCore = class {
18835
19925
  )?.id ?? null,
18836
19926
  now: this.coreRuntime.clock ? endOfLocalDay(this.coreRuntime.clock()) : null
18837
19927
  });
18838
- if (!result.ok) this._rollback();
19928
+ if (!result.ok) this._rollback(generatorSnapshot);
18839
19929
  } catch (error) {
18840
- this._rollback();
19930
+ this._rollback(generatorSnapshot);
18841
19931
  throw error;
18842
19932
  } finally {
18843
19933
  this.coreRuntime.state.endWriteCapture();
@@ -18860,19 +19950,36 @@ var ScheduleCore = class {
18860
19950
  Date.now()
18861
19951
  );
18862
19952
  }
18863
- if (result.ok && historyPolicy === "clear-on-success") {
18864
- this._saveTracker.snapshotActivities(
18865
- this.coreRuntime.state.getAllActivities()
18866
- );
18867
- this._saveTracker.snapshotLinks(this.getAllLinksView());
18868
- 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
+ }
18869
19973
  }
18870
19974
  result = this._withReappliedViewState(action, result);
18871
- if (!result.ok) return result;
19975
+ if (!result.ok) return { result, changedState: false };
18872
19976
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
18873
- 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 };
18874
19981
  }
18875
- return toPublicDispatchResult(result);
19982
+ return { result: toPublicDispatchResult(result), changedState: false };
18876
19983
  }
18877
19984
  recomputeCriticalPath() {
18878
19985
  const operation = this._enqueue(async () => ({
@@ -18882,7 +19989,11 @@ var ScheduleCore = class {
18882
19989
  return operation;
18883
19990
  }
18884
19991
  isCriticalPathSettled() {
18885
- 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;
18886
19997
  }
18887
19998
  async whenCriticalPathSettled() {
18888
19999
  while (!this.isCriticalPathSettled()) {
@@ -18890,7 +20001,11 @@ var ScheduleCore = class {
18890
20001
  }
18891
20002
  }
18892
20003
  _withReappliedViewState(action, result) {
18893
- 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;
18894
20009
  const merged = this._reapplyViewState(result.changes);
18895
20010
  return merged === null ? result : { ...result, changes: merged };
18896
20011
  }
@@ -18911,11 +20026,44 @@ var ScheduleCore = class {
18911
20026
  */
18912
20027
  _reapplyViewState(changes) {
18913
20028
  const withFilter = this._reapplyActiveFilter(changes);
18914
- const withOrder = this._reapplyActiveOrder(withFilter ?? changes);
18915
- const sequenced = this._emitTouchedBranchOrder(
18916
- 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
18917
20061
  );
18918
- return sequenced ?? withOrder ?? withFilter;
20062
+ if (createdVisibility.length === 0) return reapplied;
20063
+ return {
20064
+ ...finalChanges,
20065
+ viewState: [...finalChanges.viewState ?? [], ...createdVisibility]
20066
+ };
18919
20067
  }
18920
20068
  /**
18921
20069
  * Emits `order` for the branches this mutation resequenced, when no user order
@@ -18937,18 +20085,19 @@ var ScheduleCore = class {
18937
20085
  */
18938
20086
  _emitTouchedBranchOrder(changes) {
18939
20087
  const adapter = this.coreRuntime.state;
18940
- if (adapter.getActiveOrder() !== null) return null;
18941
- const touched = collectResequencedParents(
20088
+ const touchedParents = collectResequencedParents(
18942
20089
  changes,
18943
20090
  (activityId) => adapter.getParentId(activityId)
18944
20091
  );
18945
- if (touched.size === 0) return null;
18946
- const order = [...touched].map((parentId) => ({
18947
- parentId,
18948
- childIds: getChildrenInVisualOrder(parentId, adapter)
18949
- })).filter((branch) => branch.childIds.length > 1);
18950
- if (order.length === 0) return null;
18951
- 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 };
18952
20101
  }
18953
20102
  /**
18954
20103
  * Re-sequences the grid after any mutation that could have changed a value the
@@ -18959,23 +20108,63 @@ var ScheduleCore = class {
18959
20108
  * Production only re-sorts after a bar drag; diverging from that is a
18960
20109
  * deliberate product decision, not an oversight.
18961
20110
  */
18962
- _reapplyActiveOrder(changes) {
20111
+ _reapplyActiveOrder(changes, resequencedParents) {
18963
20112
  const adapter = this.coreRuntime.state;
18964
20113
  if (adapter.getActiveOrder() === null) return null;
18965
20114
  adapter.invalidateDerivedOrder();
18966
- 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 };
18967
20132
  }
18968
20133
  _reapplyActiveFilter(changes) {
18969
- const filter = this.coreRuntime.state.getActiveFilter();
18970
- if (filter === null) return null;
18971
20134
  const adapter = this.coreRuntime.state;
18972
- const visibleIds = evaluateVisibleIds({
18973
- activities: adapter.getAllActivities(),
18974
- 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,
18975
20141
  filter,
18976
- context: buildFilterContext(this.coreRuntime.sector.hoursPerDay)
18977
- });
18978
- 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);
18979
20168
  if (viewState.length === 0) return null;
18980
20169
  return {
18981
20170
  ...changes,
@@ -18984,6 +20173,10 @@ var ScheduleCore = class {
18984
20173
  }
18985
20174
  _recordScheduleMutation(criticalPathIsFresh) {
18986
20175
  this._scheduleRevision++;
20176
+ this._pendingScheduleRevision = Math.max(
20177
+ this._pendingScheduleRevision,
20178
+ this._scheduleRevision
20179
+ );
18987
20180
  if (this._activeCriticalPath) {
18988
20181
  this._activeCriticalPath.cancellation.cancelled = true;
18989
20182
  }
@@ -19014,23 +20207,30 @@ var ScheduleCore = class {
19014
20207
  this.coreRuntime.baseCalendars
19015
20208
  )
19016
20209
  );
19017
- 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;
19018
20215
  const promise = (async () => {
19019
20216
  const calculatedFields = await computeCriticalPathFields(
19020
20217
  isolatedState,
19021
20218
  this.coreRuntime.sector.hoursPerDay,
19022
20219
  isCurrent
19023
20220
  );
19024
- if (!isCurrent()) return null;
19025
- const fieldsByActivity = calculatedFields ?? /* @__PURE__ */ new Map();
20221
+ if (!isCurrent() || calculatedFields === null) return null;
19026
20222
  return this._enqueue(async () => {
19027
20223
  if (!isCurrent()) return null;
19028
20224
  this.coreRuntime.state.beginWriteCapture();
19029
20225
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
19030
20226
  let changes;
19031
20227
  try {
19032
- for (const [activityId, fields] of fieldsByActivity) {
19033
- this.coreRuntime.state.setActivityFields(activityId, fields);
20228
+ for (const [activityId, fields] of calculatedFields) {
20229
+ setCriticalPathFieldsIfChanged(
20230
+ this.coreRuntime.state,
20231
+ activityId,
20232
+ fields
20233
+ );
19034
20234
  }
19035
20235
  changes = await assembleChangeSet(this.coreRuntime.state, {
19036
20236
  source: { kind: "init" },
@@ -19092,47 +20292,17 @@ var ScheduleCore = class {
19092
20292
  }
19093
20293
  return { changes };
19094
20294
  }
19095
- _recomputeAfterHistoryRestore() {
19096
- const state = this.coreRuntime.state;
19097
- recomputeAllProgressRollup(state);
19098
- recomputeCanonicalRealWork(state);
19099
- emitRealCost(state);
19100
- if (this.coreRuntime.clock) {
19101
- const now = endOfLocalDay(this.coreRuntime.clock());
19102
- applyExpectedProgressLive(state, now);
19103
- let hasActiveBaseline2 = false;
19104
- state.forEachActivity((activity) => {
19105
- hasActiveBaseline2 ||= getActiveBaseline(activity) !== null;
19106
- });
19107
- if (hasActiveBaseline2) {
19108
- runExpectedProgressBase(
19109
- state,
19110
- now,
19111
- this.coreRuntime.baseCalendars.find(
19112
- (calendar) => calendar.baseDefault
19113
- )?.id ?? null
19114
- );
19115
- }
19116
- applyStatusPass(state, this.coreRuntime.sector.statusCriteria);
19117
- }
19118
- }
19119
20295
  undo(options = {}) {
20296
+ const admitted = this._admitScheduleMutation();
20297
+ let changedState = false;
19120
20298
  const operation = this._enqueue(async () => {
19121
- this.assertReady();
19122
- const entry = this._undo.takeUndo();
19123
- if (!entry) return null;
19124
- applyUndo(this.coreRuntime.state, entry);
19125
- this._recomputeAfterHistoryRestore();
19126
- this._recordScheduleMutation(false);
19127
- if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
19128
- this._undo.pushRedo(entry);
19129
- this._undo.resetCoalesce();
19130
- const changes = buildInverseChangeSet(
19131
- this.coreRuntime.state,
19132
- entry,
19133
- "before"
19134
- );
19135
- 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
+ }
19136
20306
  });
19137
20307
  void operation.then(
19138
20308
  (changes) => {
@@ -19149,23 +20319,45 @@ var ScheduleCore = class {
19149
20319
  );
19150
20320
  return operation;
19151
20321
  }
19152
- redo(options = {}) {
19153
- const operation = this._enqueue(async () => {
20322
+ async _runUndo(options, markChanged) {
20323
+ {
19154
20324
  this.assertReady();
19155
- 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();
19156
20329
  if (!entry) return null;
19157
- applyRedo(this.coreRuntime.state, entry);
19158
- this._recomputeAfterHistoryRestore();
20330
+ applyUndo(this.coreRuntime.state, entry);
19159
20331
  this._recordScheduleMutation(false);
20332
+ markChanged(true);
19160
20333
  if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
19161
- this._undo.pushUndo(entry);
20334
+ this._undo.pushRedo(entry);
19162
20335
  this._undo.resetCoalesce();
19163
20336
  const changes = buildInverseChangeSet(
19164
20337
  this.coreRuntime.state,
19165
20338
  entry,
19166
- "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)
19167
20346
  );
19168
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
+ }
19169
20361
  });
19170
20362
  void operation.then(
19171
20363
  (changes) => {
@@ -19182,6 +20374,29 @@ var ScheduleCore = class {
19182
20374
  );
19183
20375
  return operation;
19184
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
+ }
19185
20400
  canUndo() {
19186
20401
  return this._undo.canUndo();
19187
20402
  }
@@ -19222,10 +20437,17 @@ var ScheduleCore = class {
19222
20437
  );
19223
20438
  }
19224
20439
  }
19225
- _rollback() {
20440
+ _rollback(generatorSnapshot) {
19226
20441
  try {
19227
20442
  this.coreRuntime.state.restoreFromCapture();
19228
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
+ }
19229
20451
  } catch (restoreError) {
19230
20452
  this._status = SCHEDULE_CORE_STATUS.POISONED;
19231
20453
  this.coreRuntime.reporter.error(
@@ -19245,18 +20467,6 @@ function toPublicDispatchResult(result) {
19245
20467
  return publicResult;
19246
20468
  }
19247
20469
 
19248
- // src/boundary/save/link-changes.ts
19249
- function checkNoUpdatedLinks(baseline, current) {
19250
- const baselineById = new Map(
19251
- baseline.map((link) => [String(link.id), link])
19252
- );
19253
- return current.filter((link) => {
19254
- const base = baselineById.get(String(link.id));
19255
- if (!base) return false;
19256
- return base.lag !== link.lag || base.type !== link.type;
19257
- });
19258
- }
19259
-
19260
20470
  // src/boundary/save/unsaved.ts
19261
20471
  var getUnsavedActivities = (activities = []) => (activities ?? []).filter((activity) => !activity.proplannerId);
19262
20472