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