@outbuild-company/schedule-core 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,
@@ -10483,12 +10773,18 @@ async function createActivityCore(action, deps, opts = {}) {
10483
10773
  const initialTouched = /* @__PURE__ */ new Set([newId]);
10484
10774
  if (parent) initialTouched.add(parent.id);
10485
10775
  const parentWasLeaf = parent !== null && adapter.getChildren(parent.id).length === 0;
10776
+ const parentCustomId = parentWasLeaf ? parent?.customId : null;
10486
10777
  const beforeSnap = opts.skipBeforeSnap ? /* @__PURE__ */ new Map() : await snapshotActivities(adapter, initialTouched);
10487
10778
  adapter.addActivity(newActivity);
10488
10779
  applyParentMutations(action, newId, parent, adapter, parentWasLeaf);
10489
- recomputeHhCascadeForActivity(newId, adapter);
10490
- recomputeUsedCostCascadeForActivity(newId, adapter);
10491
- recomputeCostCascadeForActivity(newId, adapter);
10780
+ if (parentCustomId) {
10781
+ deps.customIdTracker.releaseCustomId(parentCustomId);
10782
+ }
10783
+ if (!opts.skipRollupCascades) {
10784
+ recomputeHhCascadeForActivity(newId, adapter);
10785
+ recomputeUsedCostCascadeForActivity(newId, adapter);
10786
+ recomputeCostCascadeForActivity(newId, adapter);
10787
+ }
10492
10788
  recomputeAndFoldCorrelatives(
10493
10789
  adapter,
10494
10790
  newId,
@@ -10660,330 +10956,96 @@ function findDefaultCalendarId(calendars) {
10660
10956
  return "global";
10661
10957
  }
10662
10958
 
10663
- // src/dispatch/handlers/activity-paste.ts
10664
- function resolvePasteRootDestination(destination, adapter) {
10665
- if ("afterSiblingId" in destination) {
10666
- const sibling = adapter.getActivity(destination.afterSiblingId);
10667
- if (!sibling) {
10668
- 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);
10669
10975
  }
10670
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
10671
- return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
10672
10976
  }
10673
- const parentId = destination.parentId;
10674
- const siblings = collectSiblings(adapter, parentId).sort(
10675
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
10676
- );
10677
- const before = siblings[destination.index]?.id;
10678
- 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;
10679
10989
  }
10680
- function applyPasteDomainSemantics(overrides) {
10681
- 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;
10682
11006
  return {
10683
- ...content,
10684
- progress: 0,
10685
- ponderator: 0,
10686
- usedCost: 0,
10687
- workHours: 0,
10688
- 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
10689
11015
  };
10690
11016
  }
10691
- async function dispatchActivityPaste(action, options, deps) {
10692
- const { adapter, scheduler, sector } = deps;
10693
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
10694
- if (!rootDest.ok) return rootDest;
10695
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
10696
- const touched = /* @__PURE__ */ new Set();
10697
- const originalToNew = /* @__PURE__ */ new Map();
10698
- const createdIds = [];
10699
- let prevRootId;
10700
- for (const input of action.activities) {
10701
- const mappedParent = originalToNew.get(input.originalParentId);
10702
- let parentId;
10703
- let afterSiblingId;
10704
- let beforeSiblingId;
10705
- if (mappedParent !== void 0) {
10706
- parentId = mappedParent;
10707
- } else {
10708
- parentId = rootDest.parentId;
10709
- if (prevRootId === void 0) {
10710
- afterSiblingId = rootDest.firstAfter;
10711
- beforeSiblingId = rootDest.firstBefore;
10712
- } else {
10713
- afterSiblingId = prevRootId;
10714
- }
10715
- }
10716
- const core = await createActivityCore(
10717
- {
10718
- parentId,
10719
- afterSiblingId,
10720
- beforeSiblingId,
10721
- ...input.activityId ? { activityId: input.activityId } : {},
10722
- overrides: applyPasteDomainSemantics(input.overrides),
10723
- eventSource: action.eventSource
10724
- },
10725
- deps,
10726
- {
10727
- skipCorrelativeRecompute: true,
10728
- skipBeforeSnap: true,
10729
- customIdReferenceId: action.referenceActivityId
10730
- }
10731
- );
10732
- if (!core.ok) return core;
10733
- if (input.baselineSnapshot !== void 0) {
10734
- const snapshot = input.baselineSnapshot;
10735
- adapter.setActivityField(
10736
- core.newId,
10737
- "baselineSnapshot",
10738
- snapshot ? {
10739
- ...snapshot,
10740
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
10741
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
10742
- } : null
10743
- );
10744
- }
10745
- originalToNew.set(input.originalId, core.newId);
10746
- createdIds.push(core.newId);
10747
- if (mappedParent === void 0) prevRootId = core.newId;
10748
- }
10749
- const correlativeShifts = recomputeCorrelativeIds(adapter);
10750
- const createdIdSet = new Set(createdIds.map(String));
10751
- foldCorrelativeShifts(
10752
- adapter,
10753
- correlativeShifts,
10754
- createdIdSet,
10755
- beforeSnap,
10756
- touched
10757
- );
10758
- for (const createdId of createdIds) touched.add(String(createdId));
10759
- const appliedLinks = [];
10760
- for (const link of action.links) {
10761
- const newSource = originalToNew.get(link.source);
10762
- const newTarget = originalToNew.get(link.target);
10763
- if (newSource === void 0 || newTarget === void 0) continue;
10764
- const op = {
10765
- kind: "create",
10766
- source: newSource,
10767
- target: newTarget,
10768
- type: link.type,
10769
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
10770
- };
10771
- const res = applyLinkOperation(op, {
10772
- port: adapter,
10773
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
10774
- });
10775
- appliedLinks.push({
10776
- op,
10777
- finalLinkId: res.applied ? res.linkId : null,
10778
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
10779
- });
10780
- }
10781
- const { scheduledIds } = await runPostMutation(
10782
- {
10783
- adapter,
10784
- scheduler,
10785
- sector: deps.sector,
10786
- defaultBaseCalendarId: deps.defaultBaseCalendarId
10787
- },
10788
- {
10789
- action,
10790
- autoscheduleFrom: createdIds[0] ?? null,
10791
- recomputeParentsFrom: createdIds,
10792
- now: deps.now,
10793
- options
10794
- }
10795
- );
10796
- const linkChanges = buildLinkChangesForBatch(
10797
- appliedLinks,
10798
- /* @__PURE__ */ new Map(),
10799
- adapter
10800
- );
10801
- return {
10802
- ok: true,
10803
- changes: await assembleChangeSet(adapter, {
10804
- source: action,
10805
- beforeSnap,
10806
- touchedIds: touched,
10807
- scheduledIds,
10808
- links: linkChanges,
10809
- hoursPerDay: sector.hoursPerDay
10810
- })
10811
- };
10812
- }
10813
-
10814
- // src/dispatch/handlers/sir-sync.ts
10815
- function dispatchSirSync(action, deps) {
10816
- const { adapter } = deps;
10817
- const id = action.activityId;
10818
- const snapshot = adapter.getActivity(id);
10819
- if (!snapshot) {
10820
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
10821
- }
10822
- const before = snapshot.pendingRequestIds ?? [];
10823
- const after = action.pendingRequests.map((r) => r.id);
10824
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
10825
- const change = {
10826
- id,
10827
- kind: "updated",
10828
- fields: { pendingRequestIds: { before, after } },
10829
- after: { id, pendingRequestIds: after }
10830
- };
10831
- return {
10832
- ok: true,
10833
- changes: {
10834
- source: action,
10835
- activities: [change],
10836
- links: [],
10837
- calendars: [],
10838
- trackingEvents: []
10839
- }
10840
- };
10841
- }
10842
-
10843
- // src/dispatch/handlers/activity-lookahead-sync.ts
10844
- function dispatchActivityLookaheadSync(action, deps) {
10845
- const ids = [...new Set(action.activityIds.map(String))];
10846
- const snapshots = ids.map((id) => deps.adapter.getActivity(id));
10847
- if (snapshots.some((snapshot) => !snapshot)) {
10848
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
10849
- }
10850
- const activities = [];
10851
- for (const [index, id] of ids.entries()) {
10852
- const snapshot = snapshots[index];
10853
- if (!snapshot) continue;
10854
- const fields = {};
10855
- if (snapshot.isLookahead !== action.isLookahead) {
10856
- fields.isLookahead = {
10857
- before: snapshot.isLookahead,
10858
- after: action.isLookahead
10859
- };
10860
- deps.adapter.setActivityField(id, "isLookahead", action.isLookahead);
10861
- }
10862
- if (snapshot.hasLookaheadTasks !== action.hasLookaheadTasks) {
10863
- fields.hasLookaheadTasks = {
10864
- before: snapshot.hasLookaheadTasks,
10865
- after: action.hasLookaheadTasks
10866
- };
10867
- deps.adapter.setActivityField(
10868
- id,
10869
- "hasLookaheadTasks",
10870
- action.hasLookaheadTasks
10871
- );
10872
- }
10873
- if (Object.keys(fields).length === 0) continue;
10874
- activities.push({
10875
- id,
10876
- kind: "updated",
10877
- fields,
10878
- after: {
10879
- ...snapshot,
10880
- isLookahead: action.isLookahead,
10881
- hasLookaheadTasks: action.hasLookaheadTasks
10882
- }
10883
- });
10884
- }
10885
- return {
10886
- ok: true,
10887
- changes: {
10888
- source: action,
10889
- activities,
10890
- links: [],
10891
- calendars: [],
10892
- trackingEvents: []
10893
- }
10894
- };
10895
- }
10896
-
10897
- // src/propagations/upward/recompute-rollup-cascades.ts
10898
- function recomputeRollupCascadesForParent(parentId, adapter) {
10899
- recomputeHhCascadeForParent(parentId, adapter);
10900
- recomputeUsedCostCascadeForParent(parentId, adapter);
10901
- recomputeCostCascadeForParent(parentId, adapter);
10902
- }
10903
-
10904
- // src/propagations/shared/traversal-helpers.ts
10905
- function collectDescendantIds(rootIds, adapter) {
10906
- const out = /* @__PURE__ */ new Set();
10907
- const stack = [];
10908
- for (const id of rootIds) {
10909
- const key = String(id);
10910
- if (!out.has(key)) {
10911
- out.add(key);
10912
- stack.push(key);
10913
- }
10914
- }
10915
- while (stack.length > 0) {
10916
- const current = stack.pop();
10917
- const children = adapter.getChildren(current);
10918
- for (const childId of children) {
10919
- const key = String(childId);
10920
- if (!out.has(key)) {
10921
- out.add(key);
10922
- stack.push(key);
10923
- }
10924
- }
10925
- }
10926
- return out;
10927
- }
10928
- function collectIncidentLinkIds(activityIds, adapter) {
10929
- const out = /* @__PURE__ */ new Set();
10930
- for (const activityId of activityIds) {
10931
- for (const linkId of adapter.getOutgoingLinkIds(activityId)) {
10932
- out.add(String(linkId));
10933
- }
10934
- for (const linkId of adapter.getIncomingLinkIds(activityId)) {
10935
- out.add(String(linkId));
10936
- }
10937
- }
10938
- return [...out];
10939
- }
10940
-
10941
- // src/internal/hierarchy/parent-demotion.ts
10942
- function restoredPromotionFields(restore) {
10943
- if (!restore) return null;
10944
- return {
10945
- type: restore.type,
10946
- startDate: new Date(restore.startDate),
10947
- endDate: new Date(restore.endDate),
10948
- durationHours: restore.durationHours,
10949
- expectedProgressBaseline: restore.expectedProgressBaseline,
10950
- constraintType: restore.constraintType,
10951
- constraintDate: restore.constraintDate ? new Date(restore.constraintDate) : null,
10952
- progress: restore.progress
10953
- };
10954
- }
10955
- function canonicalEndDateField(parent, durationHours, computeEndDate) {
10956
- const startDate = parent.startDate;
10957
- if (!computeEndDate || !(startDate instanceof Date)) return {};
10958
- return { endDate: computeEndDate(startDate, durationHours) };
10959
- }
10960
- function buildParentDemotionMutations(input) {
10961
- if (input.remainingChildIds.length > 0) return null;
10962
- const parent = input.parent;
10963
- const canonicalDuration = input.defaultDurationHours;
10964
- const hasCanonicalDefaults = Number.isFinite(canonicalDuration);
10965
- const fields = restoredPromotionFields(input.promotionSnapshot) ?? (hasCanonicalDefaults ? {
10966
- type: DEMOTION_TARGET_TYPE,
10967
- durationHours: canonicalDuration,
10968
- progress: 0,
10969
- autoScheduling: true,
10970
- constraintType: "asap",
10971
- constraintDate: null,
10972
- hasNewActivities: false,
10973
- ...canonicalEndDateField(
10974
- parent,
10975
- canonicalDuration,
10976
- input.computeEndDate
10977
- )
10978
- } : { type: DEMOTION_TARGET_TYPE });
10979
- if (input.idsRemoved && input.idsRemoved.size > 0) {
10980
- const existing = Array.isArray(parent.newActivityIds) ? parent.newActivityIds : [];
10981
- if (existing.length > 0) {
10982
- const filtered = existing.filter(
10983
- (id) => !input.idsRemoved.has(id) && !input.idsRemoved.has(String(id))
10984
- );
10985
- if (filtered.length !== existing.length) {
10986
- fields.newActivityIds = filtered;
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))
11046
+ );
11047
+ if (filtered.length !== existing.length) {
11048
+ fields.newActivityIds = filtered;
10987
11049
  }
10988
11050
  }
10989
11051
  }
@@ -11192,7 +11254,647 @@ function buildLinkDeletions(beforeLinks) {
11192
11254
  after: null
11193
11255
  });
11194
11256
  }
11195
- 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
+ };
11196
11898
  }
11197
11899
 
11198
11900
  // src/internal/hierarchy/invalid-links.ts
@@ -12790,7 +13492,10 @@ async function dispatchStatusCriteriaSet(action, deps) {
12790
13492
  }
12791
13493
 
12792
13494
  // src/dispatch/dispatch.ts
12793
- 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
+ }
12794
13499
  if (action.kind === "persistence-acknowledge") {
12795
13500
  return dispatchPersistenceAcknowledge(action, deps);
12796
13501
  }
@@ -16024,6 +16729,7 @@ var ScheduleState = class {
16024
16729
  };
16025
16730
  _writeCapture = new WriteCapture();
16026
16731
  _viewState = new ViewStateStore();
16732
+ _filterProjection = null;
16027
16733
  _viewStateBefore = null;
16028
16734
  _lastStartDateBefore = null;
16029
16735
  _promotionSnapshotBefore = null;
@@ -16138,6 +16844,13 @@ var ScheduleState = class {
16138
16844
  setActiveFilter(nextFilter) {
16139
16845
  this._captureViewStateOnce();
16140
16846
  this._viewState.setActiveFilter(nextFilter);
16847
+ this._filterProjection = null;
16848
+ }
16849
+ getFilterProjection() {
16850
+ return this._filterProjection;
16851
+ }
16852
+ setFilterProjection(projection) {
16853
+ this._filterProjection = projection;
16141
16854
  }
16142
16855
  getActiveOrder() {
16143
16856
  return this._viewState.getActiveOrder();
@@ -16179,6 +16892,7 @@ var ScheduleState = class {
16179
16892
  }
16180
16893
  hydrateViewState(seed2) {
16181
16894
  this._viewState.hydrate(seed2);
16895
+ this._filterProjection = null;
16182
16896
  this.invalidateDerivedOrder();
16183
16897
  }
16184
16898
  _captureViewStateOnce() {
@@ -16379,6 +17093,7 @@ var ScheduleState = class {
16379
17093
  this._restoreActivityFields(before);
16380
17094
  if (this._viewStateBefore !== null) {
16381
17095
  this._viewState.restore(this._viewStateBefore);
17096
+ this._filterProjection = null;
16382
17097
  this.invalidateDerivedOrder();
16383
17098
  }
16384
17099
  if (this._promotionSnapshotBefore !== null) {
@@ -16588,8 +17303,10 @@ function buildHistoryChangeSet(changes) {
16588
17303
  ...entry,
16589
17304
  after: null
16590
17305
  }));
17306
+ const source = changes.source.kind === "activity-batch" ? { ...changes.source, items: [], links: [] } : changes.source;
16591
17307
  return cloneDomainValue({
16592
17308
  ...changes,
17309
+ source,
16593
17310
  activities: activitiesWithoutAfter,
16594
17311
  links: linksWithoutAfter
16595
17312
  });
@@ -16606,7 +17323,7 @@ function captureCreatedSnapshots(state, changeSet) {
16606
17323
  if (live) afterSnap.set(String(ch.id), structuredCloneActivity(live));
16607
17324
  }
16608
17325
  for (const ch of changeSet.links) {
16609
- if (ch.kind !== "created") continue;
17326
+ if (ch.kind === "deleted") continue;
16610
17327
  const live = state.getLink(String(ch.id));
16611
17328
  if (live) afterLinks.set(String(ch.id), { ...live });
16612
17329
  }
@@ -16829,6 +17546,17 @@ function applyChangeSetSide(state, entry, side) {
16829
17546
  for (const ch of cs.links) {
16830
17547
  if (ch.kind === "updated") {
16831
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
+ }
16832
17560
  for (const [field, fv] of Object.entries(ch.fields)) {
16833
17561
  setLinkFieldDynamic(
16834
17562
  state,
@@ -17099,9 +17827,26 @@ function idArrayEquals(current, saved) {
17099
17827
  return currentIds.every((value, index) => value === savedIds[index]);
17100
17828
  }
17101
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
+
17102
17845
  // src/boundary/save/save-tracker.ts
17103
17846
  var SaveTracker = class {
17104
17847
  initialized = false;
17848
+ dirtyActivityIds = /* @__PURE__ */ new Set();
17849
+ dirtyLinkIds = /* @__PURE__ */ new Set();
17105
17850
  linksAtLastSave = /* @__PURE__ */ new Map();
17106
17851
  activitiesAtLastSave = /* @__PURE__ */ new Map();
17107
17852
  init(activities, links) {
@@ -17111,10 +17856,16 @@ var SaveTracker = class {
17111
17856
  }
17112
17857
  snapshotActivities(activities) {
17113
17858
  this.activitiesAtLastSave.clear();
17859
+ this.dirtyActivityIds.clear();
17114
17860
  for (const activity of activities) {
17115
- 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);
17116
17867
  this.activitiesAtLastSave.set(String(activity.id), {
17117
- persistable: projectPersistableActivity(activity),
17868
+ persistable,
17118
17869
  deletedSnapshot: {
17119
17870
  id: activity.id,
17120
17871
  proplannerId: activity.proplannerId,
@@ -17122,12 +17873,20 @@ var SaveTracker = class {
17122
17873
  hadBaseline: activity.baselineSnapshot !== null || activity.baselinePoints.length > 0
17123
17874
  }
17124
17875
  });
17876
+ if (activityChangedSince(activity, persistable)) {
17877
+ this.dirtyActivityIds.add(activityId);
17878
+ }
17125
17879
  }
17126
17880
  }
17127
17881
  snapshotLinks(links) {
17128
17882
  this.linksAtLastSave.clear();
17883
+ this.dirtyLinkIds.clear();
17129
17884
  for (const link of links) {
17130
- if (link.proplannerId == null) continue;
17885
+ const linkId = String(link.id);
17886
+ if (link.proplannerId == null) {
17887
+ this.dirtyLinkIds.add(linkId);
17888
+ continue;
17889
+ }
17131
17890
  this.linksAtLastSave.set(String(link.id), {
17132
17891
  id: link.id,
17133
17892
  proplannerId: link.proplannerId,
@@ -17138,21 +17897,52 @@ var SaveTracker = class {
17138
17897
  });
17139
17898
  }
17140
17899
  }
17141
- hasUnsavedChanges(activities, links) {
17142
- if (!this.initialized) return false;
17143
- if (this.deletedActivities(activities).length > 0) return true;
17144
- if (this.deletedLinks(links).length > 0) return true;
17145
- const activityChanged = activities.some((activity) => {
17146
- if (activity.proplannerId == null) return true;
17147
- const saved = this.activitiesAtLastSave.get(String(activity.id));
17148
- return !saved || activityChangedSince(activity, saved.persistable);
17149
- });
17150
- if (activityChanged) return true;
17151
- return links.some((link) => {
17152
- if (link.proplannerId == null) return true;
17153
- const saved = this.linksAtLastSave.get(String(link.id));
17154
- return !saved || link.lag !== saved.lag || link.type !== saved.type;
17155
- });
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);
17156
17946
  }
17157
17947
  deletedActivities(activities) {
17158
17948
  if (!this.initialized) return [];
@@ -17190,10 +17980,13 @@ var SaveTracker = class {
17190
17980
  if (link.proplannerId == null) return false;
17191
17981
  const saved = this.linksAtLastSave.get(String(link.id));
17192
17982
  if (!saved) return true;
17193
- return link.lag !== saved.lag || link.type !== saved.type;
17983
+ return linkChangedSince(saved, link);
17194
17984
  });
17195
17985
  }
17196
17986
  clear() {
17987
+ this.initialized = false;
17988
+ this.dirtyActivityIds.clear();
17989
+ this.dirtyLinkIds.clear();
17197
17990
  this.linksAtLastSave.clear();
17198
17991
  this.activitiesAtLastSave.clear();
17199
17992
  }
@@ -17795,6 +18588,12 @@ function createActivityIdGenerator(options = {}) {
17795
18588
  const n = typeof id === "number" ? id : Number(id);
17796
18589
  if (!Number.isFinite(n)) return;
17797
18590
  raise(n);
18591
+ },
18592
+ snapshot() {
18593
+ return counter;
18594
+ },
18595
+ restore(snapshot) {
18596
+ counter = snapshot;
17798
18597
  }
17799
18598
  };
17800
18599
  }
@@ -17820,6 +18619,12 @@ function createLinkIdGenerator(options = {}) {
17820
18619
  const n = typeof id === "number" ? id : Number(id);
17821
18620
  if (!Number.isFinite(n)) return;
17822
18621
  raise(n);
18622
+ },
18623
+ snapshot() {
18624
+ return counter;
18625
+ },
18626
+ restore(snapshot) {
18627
+ counter = snapshot;
17823
18628
  }
17824
18629
  };
17825
18630
  }
@@ -17849,6 +18654,12 @@ function createUniqueCorrelativeIdGenerator(options = {}) {
17849
18654
  if (uid2 == null) return;
17850
18655
  const n = typeof uid2 === "number" ? uid2 : Number(uid2);
17851
18656
  raise(n);
18657
+ },
18658
+ snapshot() {
18659
+ return counter;
18660
+ },
18661
+ restore(snapshot) {
18662
+ counter = snapshot;
17852
18663
  }
17853
18664
  };
17854
18665
  }
@@ -17925,13 +18736,13 @@ function minSuffixFromArray(suffixes) {
17925
18736
  if (!suffixes || suffixes.length === 0) {
17926
18737
  throw new Error("minSuffixFromArray requires non-empty array");
17927
18738
  }
17928
- return suffixes.reduce((min, current) => minSuffix(min, current));
18739
+ return suffixes.reduce((min2, current) => minSuffix(min2, current));
17929
18740
  }
17930
18741
  function maxSuffixFromArray(suffixes) {
17931
18742
  if (!suffixes || suffixes.length === 0) {
17932
18743
  throw new Error("maxSuffixFromArray requires non-empty array");
17933
18744
  }
17934
- return suffixes.reduce((max, current) => maxSuffix(max, current));
18745
+ return suffixes.reduce((max2, current) => maxSuffix(max2, current));
17935
18746
  }
17936
18747
 
17937
18748
  // src/generators/custom-id/generation-strategies.ts
@@ -18625,13 +19436,204 @@ function initializeCore(input) {
18625
19436
  };
18626
19437
  }
18627
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
+
18628
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
+ }
18629
19607
  var ScheduleCore = class {
18630
19608
  _status = SCHEDULE_CORE_STATUS.READY;
18631
19609
  coreRuntime;
18632
19610
  _undo = new UndoRecorder();
18633
19611
  _opQueue = Promise.resolve();
18634
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;
18635
19637
  _criticalPathRevision = -1;
18636
19638
  _activeCriticalPath = null;
18637
19639
  constructor(input) {
@@ -18684,6 +19686,16 @@ var ScheduleCore = class {
18684
19686
  this.assertReady();
18685
19687
  return readActivity(this.coreRuntime.state, id);
18686
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
+ }
18687
19699
  getAllActivitiesView() {
18688
19700
  this.assertReady();
18689
19701
  return readAllActivities(this.coreRuntime.state);
@@ -18781,10 +19793,7 @@ var ScheduleCore = class {
18781
19793
  }
18782
19794
  hasUnsavedChanges() {
18783
19795
  this.assertReady();
18784
- return this._saveTracker.hasUnsavedChanges(
18785
- this.coreRuntime.state.getAllActivities(),
18786
- this.getAllLinksView()
18787
- );
19796
+ return this._saveTracker.hasUnsavedChanges();
18788
19797
  }
18789
19798
  getDeletedActivitiesSinceLastSave() {
18790
19799
  this.assertReady();
@@ -18802,10 +19811,90 @@ var ScheduleCore = class {
18802
19811
  );
18803
19812
  return workPromise;
18804
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
+ }
18805
19879
  async dispatch(action, options = {}) {
18806
- 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
+ }
18807
19896
  }
18808
- async _dispatchInner(action, options = {}) {
19897
+ async _runDispatch(action, options) {
18809
19898
  this.assertReady();
18810
19899
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
18811
19900
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
@@ -18813,11 +19902,16 @@ var ScheduleCore = class {
18813
19902
  inputUnit: options.inputUnit ?? "days"
18814
19903
  });
18815
19904
  const historyPolicy = getDispatchHistoryPolicy(action);
19905
+ const generatorSnapshot = [
19906
+ this.coreRuntime.activityIdGenerator.snapshot(),
19907
+ this.coreRuntime.linkIdGenerator.snapshot(),
19908
+ this.coreRuntime.uniqueCorrelativeIdGenerator.snapshot()
19909
+ ];
18816
19910
  this.coreRuntime.state.beginWriteCapture();
18817
19911
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
18818
19912
  let result;
18819
19913
  try {
18820
- result = await dispatch(action, options, {
19914
+ result = await performDispatch(action, options, {
18821
19915
  adapter: this.coreRuntime.state,
18822
19916
  ctx: pipelineContext,
18823
19917
  scheduler: this.coreRuntime.scheduler,
@@ -18833,9 +19927,9 @@ var ScheduleCore = class {
18833
19927
  )?.id ?? null,
18834
19928
  now: this.coreRuntime.clock ? endOfLocalDay(this.coreRuntime.clock()) : null
18835
19929
  });
18836
- if (!result.ok) this._rollback();
19930
+ if (!result.ok) this._rollback(generatorSnapshot);
18837
19931
  } catch (error) {
18838
- this._rollback();
19932
+ this._rollback(generatorSnapshot);
18839
19933
  throw error;
18840
19934
  } finally {
18841
19935
  this.coreRuntime.state.endWriteCapture();
@@ -18858,19 +19952,36 @@ var ScheduleCore = class {
18858
19952
  Date.now()
18859
19953
  );
18860
19954
  }
18861
- if (result.ok && historyPolicy === "clear-on-success") {
18862
- this._saveTracker.snapshotActivities(
18863
- this.coreRuntime.state.getAllActivities()
18864
- );
18865
- this._saveTracker.snapshotLinks(this.getAllLinksView());
18866
- 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
+ }
18867
19975
  }
18868
19976
  result = this._withReappliedViewState(action, result);
18869
- if (!result.ok) return result;
19977
+ if (!result.ok) return { result, changedState: false };
18870
19978
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
18871
- 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 };
18872
19983
  }
18873
- return toPublicDispatchResult(result);
19984
+ return { result: toPublicDispatchResult(result), changedState: false };
18874
19985
  }
18875
19986
  recomputeCriticalPath() {
18876
19987
  const operation = this._enqueue(async () => ({
@@ -18880,7 +19991,11 @@ var ScheduleCore = class {
18880
19991
  return operation;
18881
19992
  }
18882
19993
  isCriticalPathSettled() {
18883
- 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;
18884
19999
  }
18885
20000
  async whenCriticalPathSettled() {
18886
20001
  while (!this.isCriticalPathSettled()) {
@@ -18888,7 +20003,11 @@ var ScheduleCore = class {
18888
20003
  }
18889
20004
  }
18890
20005
  _withReappliedViewState(action, result) {
18891
- 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;
18892
20011
  const merged = this._reapplyViewState(result.changes);
18893
20012
  return merged === null ? result : { ...result, changes: merged };
18894
20013
  }
@@ -18909,11 +20028,44 @@ var ScheduleCore = class {
18909
20028
  */
18910
20029
  _reapplyViewState(changes) {
18911
20030
  const withFilter = this._reapplyActiveFilter(changes);
18912
- const withOrder = this._reapplyActiveOrder(withFilter ?? changes);
18913
- const sequenced = this._emitTouchedBranchOrder(
18914
- 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
18915
20063
  );
18916
- return sequenced ?? withOrder ?? withFilter;
20064
+ if (createdVisibility.length === 0) return reapplied;
20065
+ return {
20066
+ ...finalChanges,
20067
+ viewState: [...finalChanges.viewState ?? [], ...createdVisibility]
20068
+ };
18917
20069
  }
18918
20070
  /**
18919
20071
  * Emits `order` for the branches this mutation resequenced, when no user order
@@ -18935,18 +20087,19 @@ var ScheduleCore = class {
18935
20087
  */
18936
20088
  _emitTouchedBranchOrder(changes) {
18937
20089
  const adapter = this.coreRuntime.state;
18938
- if (adapter.getActiveOrder() !== null) return null;
18939
- const touched = collectResequencedParents(
20090
+ const touchedParents = collectResequencedParents(
18940
20091
  changes,
18941
20092
  (activityId) => adapter.getParentId(activityId)
18942
20093
  );
18943
- if (touched.size === 0) return null;
18944
- const order = [...touched].map((parentId) => ({
18945
- parentId,
18946
- childIds: getChildrenInVisualOrder(parentId, adapter)
18947
- })).filter((branch) => branch.childIds.length > 1);
18948
- if (order.length === 0) return null;
18949
- 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 };
18950
20103
  }
18951
20104
  /**
18952
20105
  * Re-sequences the grid after any mutation that could have changed a value the
@@ -18957,23 +20110,63 @@ var ScheduleCore = class {
18957
20110
  * Production only re-sorts after a bar drag; diverging from that is a
18958
20111
  * deliberate product decision, not an oversight.
18959
20112
  */
18960
- _reapplyActiveOrder(changes) {
20113
+ _reapplyActiveOrder(changes, resequencedParents) {
18961
20114
  const adapter = this.coreRuntime.state;
18962
20115
  if (adapter.getActiveOrder() === null) return null;
18963
20116
  adapter.invalidateDerivedOrder();
18964
- 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 };
18965
20134
  }
18966
20135
  _reapplyActiveFilter(changes) {
18967
- const filter = this.coreRuntime.state.getActiveFilter();
18968
- if (filter === null) return null;
18969
20136
  const adapter = this.coreRuntime.state;
18970
- const visibleIds = evaluateVisibleIds({
18971
- activities: adapter.getAllActivities(),
18972
- 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,
18973
20143
  filter,
18974
- context: buildFilterContext(this.coreRuntime.sector.hoursPerDay)
18975
- });
18976
- 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);
18977
20170
  if (viewState.length === 0) return null;
18978
20171
  return {
18979
20172
  ...changes,
@@ -18982,6 +20175,10 @@ var ScheduleCore = class {
18982
20175
  }
18983
20176
  _recordScheduleMutation(criticalPathIsFresh) {
18984
20177
  this._scheduleRevision++;
20178
+ this._pendingScheduleRevision = Math.max(
20179
+ this._pendingScheduleRevision,
20180
+ this._scheduleRevision
20181
+ );
18985
20182
  if (this._activeCriticalPath) {
18986
20183
  this._activeCriticalPath.cancellation.cancelled = true;
18987
20184
  }
@@ -19012,23 +20209,30 @@ var ScheduleCore = class {
19012
20209
  this.coreRuntime.baseCalendars
19013
20210
  )
19014
20211
  );
19015
- 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;
19016
20217
  const promise = (async () => {
19017
20218
  const calculatedFields = await computeCriticalPathFields(
19018
20219
  isolatedState,
19019
20220
  this.coreRuntime.sector.hoursPerDay,
19020
20221
  isCurrent
19021
20222
  );
19022
- if (!isCurrent()) return null;
19023
- const fieldsByActivity = calculatedFields ?? /* @__PURE__ */ new Map();
20223
+ if (!isCurrent() || calculatedFields === null) return null;
19024
20224
  return this._enqueue(async () => {
19025
20225
  if (!isCurrent()) return null;
19026
20226
  this.coreRuntime.state.beginWriteCapture();
19027
20227
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
19028
20228
  let changes;
19029
20229
  try {
19030
- for (const [activityId, fields] of fieldsByActivity) {
19031
- this.coreRuntime.state.setActivityFields(activityId, fields);
20230
+ for (const [activityId, fields] of calculatedFields) {
20231
+ setCriticalPathFieldsIfChanged(
20232
+ this.coreRuntime.state,
20233
+ activityId,
20234
+ fields
20235
+ );
19032
20236
  }
19033
20237
  changes = await assembleChangeSet(this.coreRuntime.state, {
19034
20238
  source: { kind: "init" },
@@ -19090,47 +20294,17 @@ var ScheduleCore = class {
19090
20294
  }
19091
20295
  return { changes };
19092
20296
  }
19093
- _recomputeAfterHistoryRestore() {
19094
- const state = this.coreRuntime.state;
19095
- recomputeAllProgressRollup(state);
19096
- recomputeCanonicalRealWork(state);
19097
- emitRealCost(state);
19098
- if (this.coreRuntime.clock) {
19099
- const now = endOfLocalDay(this.coreRuntime.clock());
19100
- applyExpectedProgressLive(state, now);
19101
- let hasActiveBaseline2 = false;
19102
- state.forEachActivity((activity) => {
19103
- hasActiveBaseline2 ||= getActiveBaseline(activity) !== null;
19104
- });
19105
- if (hasActiveBaseline2) {
19106
- runExpectedProgressBase(
19107
- state,
19108
- now,
19109
- this.coreRuntime.baseCalendars.find(
19110
- (calendar) => calendar.baseDefault
19111
- )?.id ?? null
19112
- );
19113
- }
19114
- applyStatusPass(state, this.coreRuntime.sector.statusCriteria);
19115
- }
19116
- }
19117
20297
  undo(options = {}) {
20298
+ const admitted = this._admitScheduleMutation();
20299
+ let changedState = false;
19118
20300
  const operation = this._enqueue(async () => {
19119
- this.assertReady();
19120
- const entry = this._undo.takeUndo();
19121
- if (!entry) return null;
19122
- applyUndo(this.coreRuntime.state, entry);
19123
- this._recomputeAfterHistoryRestore();
19124
- this._recordScheduleMutation(false);
19125
- if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
19126
- this._undo.pushRedo(entry);
19127
- this._undo.resetCoalesce();
19128
- const changes = buildInverseChangeSet(
19129
- this.coreRuntime.state,
19130
- entry,
19131
- "before"
19132
- );
19133
- 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
+ }
19134
20308
  });
19135
20309
  void operation.then(
19136
20310
  (changes) => {
@@ -19147,23 +20321,45 @@ var ScheduleCore = class {
19147
20321
  );
19148
20322
  return operation;
19149
20323
  }
19150
- redo(options = {}) {
19151
- const operation = this._enqueue(async () => {
20324
+ async _runUndo(options, markChanged) {
20325
+ {
19152
20326
  this.assertReady();
19153
- 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();
19154
20331
  if (!entry) return null;
19155
- applyRedo(this.coreRuntime.state, entry);
19156
- this._recomputeAfterHistoryRestore();
20332
+ applyUndo(this.coreRuntime.state, entry);
19157
20333
  this._recordScheduleMutation(false);
20334
+ markChanged(true);
19158
20335
  if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
19159
- this._undo.pushUndo(entry);
20336
+ this._undo.pushRedo(entry);
19160
20337
  this._undo.resetCoalesce();
19161
20338
  const changes = buildInverseChangeSet(
19162
20339
  this.coreRuntime.state,
19163
20340
  entry,
19164
- "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)
19165
20348
  );
19166
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
+ }
19167
20363
  });
19168
20364
  void operation.then(
19169
20365
  (changes) => {
@@ -19180,6 +20376,29 @@ var ScheduleCore = class {
19180
20376
  );
19181
20377
  return operation;
19182
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
+ }
19183
20402
  canUndo() {
19184
20403
  return this._undo.canUndo();
19185
20404
  }
@@ -19220,10 +20439,17 @@ var ScheduleCore = class {
19220
20439
  );
19221
20440
  }
19222
20441
  }
19223
- _rollback() {
20442
+ _rollback(generatorSnapshot) {
19224
20443
  try {
19225
20444
  this.coreRuntime.state.restoreFromCapture();
19226
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
+ }
19227
20453
  } catch (restoreError) {
19228
20454
  this._status = SCHEDULE_CORE_STATUS.POISONED;
19229
20455
  this.coreRuntime.reporter.error(
@@ -19243,18 +20469,6 @@ function toPublicDispatchResult(result) {
19243
20469
  return publicResult;
19244
20470
  }
19245
20471
 
19246
- // src/boundary/save/link-changes.ts
19247
- function checkNoUpdatedLinks(baseline, current) {
19248
- const baselineById = new Map(
19249
- baseline.map((link) => [String(link.id), link])
19250
- );
19251
- return current.filter((link) => {
19252
- const base = baselineById.get(String(link.id));
19253
- if (!base) return false;
19254
- return base.lag !== link.lag || base.type !== link.type;
19255
- });
19256
- }
19257
-
19258
20472
  // src/boundary/save/unsaved.ts
19259
20473
  var getUnsavedActivities = (activities = []) => (activities ?? []).filter((activity) => !activity.proplannerId);
19260
20474