@outbuild-company/schedule-core 1.6.1 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -210,19 +210,19 @@ function dispatchSelectionReplace(action, deps) {
210
210
  }
211
211
 
212
212
  // src/dispatch/shared/apply-visible-set.ts
213
- function applyVisibleSet(adapter, visibleIds) {
213
+ function applyVisibleSet(adapter, visibleIds, activityIds = adapter.getAllIds()) {
214
214
  const viewState = [];
215
- adapter.forEachActivity((_snapshot, activityId) => {
215
+ for (const activityId of activityIds) {
216
216
  const id = String(activityId);
217
217
  const willBeVisible = visibleIds.has(id);
218
218
  const before = adapter.isVisible(id);
219
- if (before === willBeVisible) return;
219
+ if (before === willBeVisible) continue;
220
220
  adapter.setVisible(id, willBeVisible);
221
221
  viewState.push({
222
222
  activityId: id,
223
223
  visible: { before, after: willBeVisible }
224
224
  });
225
- });
225
+ }
226
226
  return viewState;
227
227
  }
228
228
 
@@ -496,6 +496,12 @@ function overlapsRange(activity, range) {
496
496
  const windowStartsInside = windowStart >= taskStart && windowStart <= taskEnd;
497
497
  return fullyInside || startsInside || windowStartsInside;
498
498
  }
499
+ function matchesFilter(activity, filter, context) {
500
+ const passesCriteria = matchesCriteria(activity, filter, context);
501
+ const range = filter.dateRange;
502
+ const passesRange = range === void 0 || overlapsRange(activity, range);
503
+ return passesCriteria && passesRange;
504
+ }
499
505
  function addAncestors(matchedId, parentOf, visibleIds) {
500
506
  let ancestorId = parentOf(matchedId);
501
507
  while (ancestorId !== null && !visibleIds.has(String(ancestorId))) {
@@ -505,13 +511,10 @@ function addAncestors(matchedId, parentOf, visibleIds) {
505
511
  }
506
512
  function evaluateVisibleIds(input) {
507
513
  const { activities, parentOf, filter, context } = input;
508
- const range = filter.dateRange;
509
514
  const visibleIds = /* @__PURE__ */ new Set();
510
515
  const matchedIds = [];
511
516
  for (const activity of activities) {
512
- const passesCriteria = matchesCriteria(activity, filter, context);
513
- const passesRange = range === void 0 || overlapsRange(activity, range);
514
- if (passesCriteria && passesRange) {
517
+ if (matchesFilter(activity, filter, context)) {
515
518
  visibleIds.add(String(activity.id));
516
519
  matchedIds.push(activity.id);
517
520
  }
@@ -521,6 +524,114 @@ function evaluateVisibleIds(input) {
521
524
  }
522
525
  return visibleIds;
523
526
  }
527
+ function seedFilterProjection(input) {
528
+ const { activities, parentOf, filter, context } = input;
529
+ const matchedIds = /* @__PURE__ */ new Set();
530
+ const pinnedVisibleIds = /* @__PURE__ */ new Set();
531
+ const visibleReferenceCounts = /* @__PURE__ */ new Map();
532
+ const parentIds = /* @__PURE__ */ new Map();
533
+ const remainingChildren = /* @__PURE__ */ new Map();
534
+ for (const activity of activities) {
535
+ const activityId = String(activity.id);
536
+ const parentId = parentOf(activityId);
537
+ parentIds.set(activityId, parentId);
538
+ remainingChildren.set(activityId, remainingChildren.get(activityId) ?? 0);
539
+ if (parentId !== null) {
540
+ remainingChildren.set(
541
+ parentId,
542
+ (remainingChildren.get(parentId) ?? 0) + 1
543
+ );
544
+ }
545
+ const isMatch = matchesFilter(activity, filter, context);
546
+ const isPinned = input.pinnedVisibleIds?.has(activityId) === true;
547
+ visibleReferenceCounts.set(
548
+ activityId,
549
+ (isMatch ? 1 : 0) + (isPinned ? 1 : 0)
550
+ );
551
+ if (isMatch) matchedIds.add(activityId);
552
+ if (isPinned) pinnedVisibleIds.add(activityId);
553
+ }
554
+ return {
555
+ matchedIds,
556
+ pinnedVisibleIds,
557
+ visibleReferenceCounts,
558
+ parentIds,
559
+ remainingChildren
560
+ };
561
+ }
562
+ function propagateVisibleReferenceCounts(seed2) {
563
+ const { visibleReferenceCounts, parentIds, remainingChildren } = seed2;
564
+ const pendingIds = [];
565
+ for (const activityId of parentIds.keys()) {
566
+ if (remainingChildren.get(activityId) === 0) pendingIds.push(activityId);
567
+ }
568
+ while (pendingIds.length > 0) {
569
+ const activityId = pendingIds.pop();
570
+ if (activityId === void 0) break;
571
+ const parentId = parentIds.get(activityId);
572
+ if (parentId === null || parentId === void 0) continue;
573
+ const referenceCount = visibleReferenceCounts.get(activityId) ?? 0;
574
+ visibleReferenceCounts.set(
575
+ parentId,
576
+ (visibleReferenceCounts.get(parentId) ?? 0) + referenceCount
577
+ );
578
+ const remaining = (remainingChildren.get(parentId) ?? 1) - 1;
579
+ remainingChildren.set(parentId, remaining);
580
+ if (remaining === 0) pendingIds.push(parentId);
581
+ }
582
+ }
583
+ function collectVisibleIds(visibleReferenceCounts) {
584
+ const visibleIds = /* @__PURE__ */ new Set();
585
+ for (const [activityId, referenceCount] of visibleReferenceCounts) {
586
+ if (referenceCount > 0) visibleIds.add(activityId);
587
+ }
588
+ return visibleIds;
589
+ }
590
+ function buildFilterProjection(input) {
591
+ const seed2 = seedFilterProjection(input);
592
+ propagateVisibleReferenceCounts(seed2);
593
+ const visibleIds = collectVisibleIds(seed2.visibleReferenceCounts);
594
+ return {
595
+ filter: input.filter,
596
+ context: input.context,
597
+ matchedIds: seed2.matchedIds,
598
+ pinnedVisibleIds: seed2.pinnedVisibleIds,
599
+ visibleIds,
600
+ visibleReferenceCounts: seed2.visibleReferenceCounts
601
+ };
602
+ }
603
+ function updateFilterProjection(projection, activities, parentOf) {
604
+ const changedVisibilityIds = /* @__PURE__ */ new Set();
605
+ for (const activity of activities) {
606
+ const activityId = String(activity.id);
607
+ changedVisibilityIds.add(activityId);
608
+ const wasMatch = projection.matchedIds.has(activityId);
609
+ const isMatch = matchesFilter(
610
+ activity,
611
+ projection.filter,
612
+ projection.context
613
+ );
614
+ if (wasMatch === isMatch) continue;
615
+ if (isMatch) projection.matchedIds.add(activityId);
616
+ else projection.matchedIds.delete(activityId);
617
+ const referenceDelta = isMatch ? 1 : -1;
618
+ let currentId = activityId;
619
+ while (currentId !== null) {
620
+ const beforeCount = projection.visibleReferenceCounts.get(currentId) ?? 0;
621
+ const afterCount = beforeCount + referenceDelta;
622
+ projection.visibleReferenceCounts.set(currentId, afterCount);
623
+ const wasVisible = beforeCount > 0;
624
+ const isVisible = afterCount > 0;
625
+ if (wasVisible !== isVisible) {
626
+ changedVisibilityIds.add(currentId);
627
+ if (isVisible) projection.visibleIds.add(currentId);
628
+ else projection.visibleIds.delete(currentId);
629
+ }
630
+ currentId = parentOf(currentId);
631
+ }
632
+ }
633
+ return changedVisibilityIds;
634
+ }
524
635
 
525
636
  // src/internal/filter/context.ts
526
637
  var COLLATION_LOCALE = "en";
@@ -581,17 +692,26 @@ function isValidDateValue(value) {
581
692
  function isEmptyFilter(filter) {
582
693
  return filter.criteria.length === 0 && filter.dateRange === void 0;
583
694
  }
584
- function resolveVisibleIds(adapter, filter, hoursPerDay) {
695
+ function resolveFilter(adapter, filter, hoursPerDay) {
585
696
  const activities = adapter.getAllActivities();
586
697
  if (isEmptyFilter(filter)) {
587
- return new Set(activities.map((activity) => String(activity.id)));
698
+ return {
699
+ visibleIds: evaluateVisibleIds({
700
+ activities,
701
+ parentOf: (activityId) => adapter.getParentId(activityId),
702
+ filter,
703
+ context: buildFilterContext(hoursPerDay)
704
+ }),
705
+ projection: null
706
+ };
588
707
  }
589
- return evaluateVisibleIds({
708
+ const projection = buildFilterProjection({
590
709
  activities,
591
710
  parentOf: (activityId) => adapter.getParentId(activityId),
592
711
  filter,
593
712
  context: buildFilterContext(hoursPerDay)
594
713
  });
714
+ return { visibleIds: projection.visibleIds, projection };
595
715
  }
596
716
  function dispatchFilterSet(action, deps) {
597
717
  const { adapter, hoursPerDay } = deps;
@@ -605,7 +725,12 @@ function dispatchFilterSet(action, deps) {
605
725
  dateRange: action.dateRange
606
726
  };
607
727
  adapter.setActiveFilter(isEmptyFilter(filter) ? null : filter);
608
- const visibleIds = resolveVisibleIds(adapter, filter, hoursPerDay);
728
+ const { visibleIds, projection } = resolveFilter(
729
+ adapter,
730
+ filter,
731
+ hoursPerDay
732
+ );
733
+ adapter.setFilterProjection(projection);
609
734
  const viewState = applyVisibleSet(adapter, visibleIds);
610
735
  const changes = {
611
736
  source: action,
@@ -676,11 +801,7 @@ function visualIndexInParent(taskId, adapter) {
676
801
  return siblings.findIndex((id) => String(id) === String(taskId));
677
802
  }
678
803
  function collectChildrenSnapshots(parentId, adapter) {
679
- const key = String(parentId);
680
- if (key === "0") {
681
- return adapter.getAllActivities().filter((a) => a.parentId === null);
682
- }
683
- const childIds = adapter.getChildren(parentId);
804
+ const childIds = String(parentId) === ROOT_PARENT_ID ? adapter.getRootIds?.() ?? adapter.getChildren(ROOT_PARENT_ID) : adapter.getChildren(parentId);
684
805
  const out = [];
685
806
  for (const id of childIds) {
686
807
  const snap = adapter.getActivity(id);
@@ -724,6 +845,14 @@ function collectBranchOrder(adapter) {
724
845
  visit(ROOT_PARENT_ID);
725
846
  return branches;
726
847
  }
848
+ function collectBranchOrderForParents(adapter, parentIds) {
849
+ const branches = [];
850
+ for (const parentId of parentIds) {
851
+ const childIds = getChildrenInVisualOrder(parentId, adapter);
852
+ if (childIds.length > 1) branches.push({ parentId, childIds });
853
+ }
854
+ return branches;
855
+ }
727
856
 
728
857
  // src/dispatch/order.ts
729
858
  function validateRule(rule) {
@@ -2275,6 +2404,34 @@ function wouldIntroduceCycle(existingLinks, proposedLink) {
2275
2404
  }
2276
2405
  return hasPath(adjacency, tgt, src);
2277
2406
  }
2407
+ function hasDirectedCycle(links) {
2408
+ const adjacency = /* @__PURE__ */ new Map();
2409
+ const indegrees = /* @__PURE__ */ new Map();
2410
+ for (const link of links) {
2411
+ const source = String(link.source);
2412
+ const target = String(link.target);
2413
+ const neighbors = adjacency.get(source) ?? [];
2414
+ neighbors.push(target);
2415
+ adjacency.set(source, neighbors);
2416
+ indegrees.set(source, indegrees.get(source) ?? 0);
2417
+ indegrees.set(target, (indegrees.get(target) ?? 0) + 1);
2418
+ }
2419
+ const ready = new Queue();
2420
+ for (const [activityId, indegree] of indegrees) {
2421
+ if (indegree === 0) ready.enqueue(activityId);
2422
+ }
2423
+ let visited = 0;
2424
+ while (!ready.isEmpty()) {
2425
+ const current = ready.dequeue();
2426
+ visited += 1;
2427
+ for (const neighbor of adjacency.get(current) ?? []) {
2428
+ const indegree = indegrees.get(neighbor) - 1;
2429
+ indegrees.set(neighbor, indegree);
2430
+ if (indegree === 0) ready.enqueue(neighbor);
2431
+ }
2432
+ }
2433
+ return visited !== indegrees.size;
2434
+ }
2278
2435
  function hasPath(adjacency, from, to) {
2279
2436
  if (from === to) return true;
2280
2437
  const visited = /* @__PURE__ */ new Set([from]);
@@ -2304,6 +2461,70 @@ function applyLinkOperation(op, deps) {
2304
2461
  return applyDelete(op, deps);
2305
2462
  }
2306
2463
  }
2464
+ function applyLinkCreates(links, deps) {
2465
+ if (links.length === 0) return { applied: true };
2466
+ const existingLinks = deps.port.getAllLinks();
2467
+ const duplicateKeys = /* @__PURE__ */ new Map();
2468
+ const edges = [];
2469
+ for (const link of existingLinks) {
2470
+ const key = linkKey(link);
2471
+ if (!duplicateKeys.has(key)) duplicateKeys.set(key, link.id);
2472
+ edges.push({ source: link.source, target: link.target });
2473
+ }
2474
+ const existingGraphHasCycle = hasDirectedCycle(edges);
2475
+ let rejected = null;
2476
+ for (const { operation, linkId } of links) {
2477
+ if (String(operation.source) === String(operation.target)) {
2478
+ rejected = { applied: false, rejected: "self-link" };
2479
+ break;
2480
+ }
2481
+ if (!deps.port.getActivity(operation.source) || !deps.port.getActivity(operation.target)) {
2482
+ rejected = { applied: false, rejected: "activity-missing" };
2483
+ break;
2484
+ }
2485
+ const key = linkKey(operation);
2486
+ const duplicateId = duplicateKeys.get(key);
2487
+ if (duplicateId !== void 0) {
2488
+ rejected = {
2489
+ applied: false,
2490
+ rejected: "duplicate",
2491
+ linkId: duplicateId
2492
+ };
2493
+ break;
2494
+ }
2495
+ duplicateKeys.set(key, linkId);
2496
+ if (existingGraphHasCycle && // ponytail: preserve legacy sequential semantics; optimize with dynamic
2497
+ // SCC reachability only if corrupt cyclic schedules become measurable.
2498
+ wouldIntroduceCycle(edges, {
2499
+ source: operation.source,
2500
+ target: operation.target
2501
+ })) {
2502
+ return { applied: false, rejected: "cycle" };
2503
+ }
2504
+ edges.push({ source: operation.source, target: operation.target });
2505
+ }
2506
+ if (!existingGraphHasCycle && hasDirectedCycle(edges)) {
2507
+ return { applied: false, rejected: "cycle" };
2508
+ }
2509
+ if (rejected) return rejected;
2510
+ for (const { operation, linkId } of links) {
2511
+ const preserved = deps.preserved?.get(linkId);
2512
+ deps.port.addLink({
2513
+ id: linkId,
2514
+ source: operation.source,
2515
+ target: operation.target,
2516
+ type: operation.type,
2517
+ lag: operation.lag,
2518
+ ...preserved?.ganttId === void 0 ? {} : { ganttId: preserved.ganttId },
2519
+ ...preserved?.sectorId === void 0 ? {} : { sectorId: preserved.sectorId },
2520
+ ...preserved?.proplannerId === void 0 ? {} : { proplannerId: preserved.proplannerId }
2521
+ });
2522
+ }
2523
+ return { applied: true };
2524
+ }
2525
+ function linkKey(link) {
2526
+ return JSON.stringify([String(link.source), String(link.target), link.type]);
2527
+ }
2307
2528
  function applyCreate(op, deps) {
2308
2529
  const { port } = deps;
2309
2530
  if (String(op.source) === String(op.target)) {
@@ -2427,14 +2648,14 @@ function diffLinks(input) {
2427
2648
  unresolved.push(spec);
2428
2649
  continue;
2429
2650
  }
2430
- const key = linkKey(source, spec.type);
2651
+ const key = linkKey2(source, spec.type);
2431
2652
  if (desired.has(key)) continue;
2432
2653
  desired.set(key, { source, type: spec.type, lag: spec.lag });
2433
2654
  }
2434
2655
  const currentByKey = /* @__PURE__ */ new Map();
2435
2656
  for (const link of currentLinks) {
2436
2657
  if (String(link.target) !== String(targetActivityId)) continue;
2437
- currentByKey.set(linkKey(link.source, link.type), link);
2658
+ currentByKey.set(linkKey2(link.source, link.type), link);
2438
2659
  }
2439
2660
  const operations = [];
2440
2661
  for (const [key, link] of currentByKey) {
@@ -2461,7 +2682,7 @@ function diffLinks(input) {
2461
2682
  }
2462
2683
  return { operations, unresolved };
2463
2684
  }
2464
- function linkKey(source, type) {
2685
+ function linkKey2(source, type) {
2465
2686
  return `${String(source)}::${type}`;
2466
2687
  }
2467
2688
  function diffOutgoingLinks(input) {
@@ -2474,14 +2695,14 @@ function diffOutgoingLinks(input) {
2474
2695
  unresolved.push(spec);
2475
2696
  continue;
2476
2697
  }
2477
- const key = linkKey(target, spec.type);
2698
+ const key = linkKey2(target, spec.type);
2478
2699
  if (desired.has(key)) continue;
2479
2700
  desired.set(key, { target, type: spec.type, lag: spec.lag });
2480
2701
  }
2481
2702
  const currentByKey = /* @__PURE__ */ new Map();
2482
2703
  for (const link of currentLinks) {
2483
2704
  if (String(link.source) !== String(sourceActivityId)) continue;
2484
- currentByKey.set(linkKey(link.target, link.type), link);
2705
+ currentByKey.set(linkKey2(link.target, link.type), link);
2485
2706
  }
2486
2707
  const operations = [];
2487
2708
  for (const [key, link] of currentByKey) {
@@ -4202,29 +4423,6 @@ function toConstraintWarning(activityId, notification) {
4202
4423
  };
4203
4424
  }
4204
4425
 
4205
- // src/constants/activity-properties.ts
4206
- var ACTIVITY_PROPERTY = {
4207
- PARENT: "parentId",
4208
- TYPE: "type",
4209
- START_DATE: "startDate",
4210
- END_DATE: "endDate",
4211
- CONSTRAINT_TYPE: "constraintType",
4212
- CONSTRAINT_DATE: "constraintDate",
4213
- AUTO_SCHEDULING: "autoScheduling",
4214
- PROGRESS: "progress",
4215
- COST: "cost",
4216
- USED_COST: "usedCost",
4217
- REAL_COST: "realCost",
4218
- WORK_HOURS: "workHours",
4219
- REAL_WORK_HOURS: "realWorkHours",
4220
- CORRELATIVE_ID: "correlativeId",
4221
- CUSTOM_ID: "customId",
4222
- NEW_ACTIVITIES_ARRAY: "newActivityIds",
4223
- HAS_NEW_ACTIVITIES: "hasNewActivities"};
4224
- var LINK_PROPERTY = {
4225
- TYPE: "type",
4226
- LAG: "lag"};
4227
-
4228
4426
  // src/columns/types.ts
4229
4427
  var COLUMN_WRITABLE_FIELD_LIST = [
4230
4428
  "name",
@@ -5548,7 +5746,7 @@ var CalculateBackwardParentsLinks = class {
5548
5746
  return false;
5549
5747
  }
5550
5748
  return allLinksCalculations.reduce(
5551
- (min, activity) => activity.lf < min.lf ? activity : min
5749
+ (min2, activity) => activity.lf < min2.lf ? activity : min2
5552
5750
  );
5553
5751
  }
5554
5752
  calculateSfRestrictionDates() {
@@ -7721,13 +7919,13 @@ var LinksConstraintCalculator2 = class extends CalculationsGenericMethods_defaul
7721
7919
  getMinDateFromLinks(allLinksCalculations) {
7722
7920
  try {
7723
7921
  if (this.constraint === CONSTRAINT_TYPES.ALAP || this.constraint === CONSTRAINT_TYPES.ASAP) {
7724
- const min = allLinksCalculations.reduce(
7725
- (min2, activity) => activity.lf < min2.lf ? activity : min2
7922
+ const min2 = allLinksCalculations.reduce(
7923
+ (min3, activity) => activity.lf < min3.lf ? activity : min3
7726
7924
  );
7727
- return min;
7925
+ return min2;
7728
7926
  }
7729
7927
  return allLinksCalculations.reduce(
7730
- (min, activity) => activity.ls < min.ls ? activity : min
7928
+ (min2, activity) => activity.ls < min2.ls ? activity : min2
7731
7929
  );
7732
7930
  } catch (error) {
7733
7931
  throw new Error(
@@ -7737,7 +7935,7 @@ var LinksConstraintCalculator2 = class extends CalculationsGenericMethods_defaul
7737
7935
  }
7738
7936
  getMinLfFromLinks(allLinksCalculations) {
7739
7937
  return allLinksCalculations.reduce(
7740
- (min, activity) => activity.lf < min.lf ? activity : min
7938
+ (min2, activity) => activity.lf < min2.lf ? activity : min2
7741
7939
  );
7742
7940
  }
7743
7941
  getMinDateBasedOnConstraint(minCalculatedDateFromLinks, constraintType = this.constraint) {
@@ -8195,7 +8393,7 @@ var BackwardPath = class extends generic_calculations_default {
8195
8393
  return false;
8196
8394
  }
8197
8395
  return allLinksCalculations.reduce(
8198
- (min, activity) => activity.lf < min.lf ? activity : min
8396
+ (min2, activity) => activity.lf < min2.lf ? activity : min2
8199
8397
  );
8200
8398
  }
8201
8399
  };
@@ -8794,7 +8992,7 @@ var CriticalPath = class {
8794
8992
  activitiesCount: this.forwardPathCalculations?.size || 0
8795
8993
  }
8796
8994
  });
8797
- return false;
8995
+ throw error;
8798
8996
  }
8799
8997
  }
8800
8998
  async calculateFloatsAsync(forward, backward, isCurrent) {
@@ -8815,7 +9013,7 @@ var CriticalPath = class {
8815
9013
  activitiesCount: this.forwardPathCalculations?.size || 0
8816
9014
  }
8817
9015
  });
8818
- return { totalFloatMap: /* @__PURE__ */ new Map(), freeFloatMap: /* @__PURE__ */ new Map() };
9016
+ throw error;
8819
9017
  }
8820
9018
  }
8821
9019
  };
@@ -8840,16 +9038,41 @@ function constraintDateForCriticalPath(activity, state) {
8840
9038
  return typedDayIsWorking ? getEffectiveConstraintDate(activity, state) : raw;
8841
9039
  }
8842
9040
  function toLegacyCalendar(api) {
9041
+ const endDateByKey = /* @__PURE__ */ new Map();
9042
+ const durationByKey = /* @__PURE__ */ new Map();
9043
+ const closestWorkTimeByKey = /* @__PURE__ */ new Map();
8843
9044
  return {
8844
- calculateEndDate: ({ start_date, duration }) => api.calculateEndDate(start_date, duration),
8845
- calculateDuration: ({ start_date, end_date }) => api.calculateDuration(start_date, end_date),
9045
+ calculateEndDate: ({ start_date, duration }) => {
9046
+ const key = `${start_date.getTime()}|${duration}`;
9047
+ const cached = endDateByKey.get(key);
9048
+ if (cached !== void 0) return new Date(cached);
9049
+ const value = api.calculateEndDate(start_date, duration);
9050
+ endDateByKey.set(key, value.getTime());
9051
+ return value;
9052
+ },
9053
+ calculateDuration: ({ start_date, end_date }) => {
9054
+ const key = `${start_date.getTime()}|${end_date.getTime()}`;
9055
+ const cached = durationByKey.get(key);
9056
+ if (cached !== void 0) return cached;
9057
+ const value = api.calculateDuration(start_date, end_date);
9058
+ durationByKey.set(key, value);
9059
+ return value;
9060
+ },
8846
9061
  getWorkHours: (date2) => api.getWorkHours(date2),
8847
9062
  $gantt: {
8848
- getClosestWorkTime: ({ date: date2, dir, unit }) => api.getClosestWorkTime({
8849
- date: date2,
8850
- direction: dir,
8851
- unit: unit ?? "hour"
8852
- })
9063
+ getClosestWorkTime: ({ date: date2, dir, unit }) => {
9064
+ const resolvedUnit = unit ?? "hour";
9065
+ const key = `${date2.getTime()}|${dir}|${resolvedUnit}`;
9066
+ const cached = closestWorkTimeByKey.get(key);
9067
+ if (cached !== void 0) return new Date(cached);
9068
+ const value = api.getClosestWorkTime({
9069
+ date: date2,
9070
+ direction: dir,
9071
+ unit: resolvedUnit
9072
+ });
9073
+ closestWorkTimeByKey.set(key, value.getTime());
9074
+ return value;
9075
+ }
8853
9076
  }
8854
9077
  };
8855
9078
  }
@@ -8929,19 +9152,19 @@ function orderByTree(leveled) {
8929
9152
  return ordered;
8930
9153
  }
8931
9154
  function projectBounds(state) {
8932
- let min = null;
8933
- let max = null;
9155
+ let min2 = null;
9156
+ let max2 = null;
8934
9157
  state.forEachActivity((activity) => {
8935
9158
  if (activity.startDate) {
8936
9159
  const start = activity.startDate.getTime();
8937
- min = min == null ? start : Math.min(min, start);
9160
+ min2 = min2 == null ? start : Math.min(min2, start);
8938
9161
  }
8939
9162
  if (activity.endDate) {
8940
9163
  const end = activity.endDate.getTime();
8941
- max = max == null ? end : Math.max(max, end);
9164
+ max2 = max2 == null ? end : Math.max(max2, end);
8942
9165
  }
8943
9166
  });
8944
- return { start_date: new Date(min ?? 0), end_date: new Date(max ?? 0) };
9167
+ return { start_date: new Date(min2 ?? 0), end_date: new Date(max2 ?? 0) };
8945
9168
  }
8946
9169
  async function createGanttAdapter(state, hoursPerDay) {
8947
9170
  const calendarCache = /* @__PURE__ */ new Map();
@@ -9008,13 +9231,27 @@ function fieldsForActivity(activityId, progress, maps) {
9008
9231
  freeSlackHours: maps.freeFloat.get(activityId)?.activityFreeFloat ?? null
9009
9232
  };
9010
9233
  }
9234
+ var datesEqual = (left, right) => left === right || left !== null && right !== null && left.getTime() === right.getTime();
9235
+ function setCriticalPathFieldsIfChanged(state, activityId, fields) {
9236
+ const activity = state.getActivity(activityId);
9237
+ const current = activity?.criticalPath;
9238
+ const next = fields.criticalPath;
9239
+ 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)) {
9240
+ return;
9241
+ }
9242
+ state.setActivityFields(activityId, fields);
9243
+ }
9011
9244
  async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => true) {
9012
9245
  if (!isCurrent()) return null;
9013
9246
  const gantt = await createGanttAdapter(state, hoursPerDay);
9014
9247
  if (!isCurrent()) return null;
9015
9248
  const criticalPath = new legacy_default(gantt);
9016
9249
  const calculated = await criticalPath.calculate(gantt, isCurrent);
9017
- if (!calculated || !isCurrent()) return null;
9250
+ if (!calculated) {
9251
+ if (!isCurrent()) return null;
9252
+ throw new Error("critical path calculation aborted");
9253
+ }
9254
+ if (!isCurrent()) return null;
9018
9255
  const maps = {
9019
9256
  forward: criticalPath.forwardPathCalculations,
9020
9257
  backward: criticalPath.backwardPathCalculations,
@@ -9049,11 +9286,33 @@ async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9049
9286
  if (!fieldsByActivity || !isCurrent()) return false;
9050
9287
  for (const [activityId, fields] of fieldsByActivity) {
9051
9288
  if (!isCurrent()) return false;
9052
- state.setActivityFields(activityId, fields);
9289
+ setCriticalPathFieldsIfChanged(state, activityId, fields);
9053
9290
  }
9054
9291
  return true;
9055
9292
  }
9056
9293
 
9294
+ // src/constants/activity-properties.ts
9295
+ var ACTIVITY_PROPERTY = {
9296
+ PARENT: "parentId",
9297
+ TYPE: "type",
9298
+ START_DATE: "startDate",
9299
+ END_DATE: "endDate",
9300
+ CONSTRAINT_TYPE: "constraintType",
9301
+ CONSTRAINT_DATE: "constraintDate",
9302
+ PROGRESS: "progress",
9303
+ COST: "cost",
9304
+ USED_COST: "usedCost",
9305
+ REAL_COST: "realCost",
9306
+ WORK_HOURS: "workHours",
9307
+ REAL_WORK_HOURS: "realWorkHours",
9308
+ CORRELATIVE_ID: "correlativeId",
9309
+ CUSTOM_ID: "customId",
9310
+ NEW_ACTIVITIES_ARRAY: "newActivityIds",
9311
+ HAS_NEW_ACTIVITIES: "hasNewActivities"};
9312
+ var LINK_PROPERTY = {
9313
+ TYPE: "type",
9314
+ LAG: "lag"};
9315
+
9057
9316
  // src/propagations/upward/real-cost.ts
9058
9317
  function emitRealCost(state) {
9059
9318
  const rootIds = [];
@@ -9526,6 +9785,40 @@ function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
9526
9785
  if (!finalLinkId) continue;
9527
9786
  const after = adapter.getLink(finalLinkId);
9528
9787
  if (!after) continue;
9788
+ const before = beforeLinks.get(finalLinkId);
9789
+ if (before) {
9790
+ const fields2 = {};
9791
+ if (String(before.source) !== String(after.source)) {
9792
+ fields2.source = {
9793
+ before: String(before.source),
9794
+ after: String(after.source)
9795
+ };
9796
+ }
9797
+ if (String(before.target) !== String(after.target)) {
9798
+ fields2.target = {
9799
+ before: String(before.target),
9800
+ after: String(after.target)
9801
+ };
9802
+ }
9803
+ if (before.type !== after.type) {
9804
+ fields2.type = { before: before.type, after: after.type };
9805
+ }
9806
+ if (Number(before.lag) !== Number(after.lag)) {
9807
+ fields2.lag = {
9808
+ before: Number(before.lag),
9809
+ after: Number(after.lag)
9810
+ };
9811
+ }
9812
+ if (Object.keys(fields2).length > 0) {
9813
+ out.push({
9814
+ id: String(finalLinkId),
9815
+ kind: "updated",
9816
+ fields: fields2,
9817
+ after: snapshotToLink(after)
9818
+ });
9819
+ }
9820
+ continue;
9821
+ }
9529
9822
  out.push({
9530
9823
  id: String(finalLinkId),
9531
9824
  kind: "created",
@@ -9540,6 +9833,7 @@ function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
9540
9833
  continue;
9541
9834
  }
9542
9835
  if (op.kind === "delete") {
9836
+ if (adapter.getLink(op.linkId)) continue;
9543
9837
  const beforeLink2 = beforeLinks.get(op.linkId);
9544
9838
  out.push({
9545
9839
  id: String(op.linkId),
@@ -9618,16 +9912,14 @@ async function dispatchInlineEdit(action, options, deps) {
9618
9912
  const { adapter, ctx, scheduler, sector } = deps;
9619
9913
  const pipeline = findPipelineForColumn(action.column);
9620
9914
  if (!pipeline) {
9621
- throw new Error(
9622
- `[ScheduleCore] no pipeline registered for column "${action.column}"`
9623
- );
9915
+ return {
9916
+ ok: false,
9917
+ reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
9918
+ };
9624
9919
  }
9625
- if (columnInvalidatesExpandedLinks(action.column)) {
9920
+ if (action.column === COLUMN.DURATION) {
9626
9921
  scheduler.invalidateExpandedLinksCache();
9627
9922
  }
9628
- if (action.column === ACTIVITY_PROPERTY.PARENT) {
9629
- scheduler.invalidateAllCaches();
9630
- }
9631
9923
  const activitySnap = ctx.activityReader.getActivity(action.activityId);
9632
9924
  if (!activitySnap) {
9633
9925
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
@@ -9758,9 +10050,6 @@ async function dispatchInlineEdit(action, options, deps) {
9758
10050
  })
9759
10051
  };
9760
10052
  }
9761
- function columnInvalidatesExpandedLinks(column) {
9762
- return column === COLUMN.DURATION || column === ACTIVITY_PROPERTY.AUTO_SCHEDULING;
9763
- }
9764
10053
 
9765
10054
  // src/dispatch/handlers/link.ts
9766
10055
  function collectBatchContext(operations, adapter) {
@@ -10270,11 +10559,24 @@ function buildNewActivity(input, adapter) {
10270
10559
  startDate: rawStartDate,
10271
10560
  endDate: rawStartDate
10272
10561
  };
10273
- const startDate = overrides.startDate ? new Date(base.startDate) : adapter.getClosestWorkTime({
10562
+ let startDate = overrides.startDate ? new Date(base.startDate) : adapter.getClosestWorkTime({
10274
10563
  date: base.startDate,
10275
10564
  dir: WORK_TIME_DIRECTION.FUTURE,
10276
10565
  task: base
10277
10566
  });
10567
+ if (!overrides.startDate && parent && isMilestoneType(parent.type)) {
10568
+ const inheritedDay = new Date(parent.startDate);
10569
+ inheritedDay.setUTCHours(0, 0, 0, 0);
10570
+ const snappedDay = new Date(startDate);
10571
+ snappedDay.setUTCHours(0, 0, 0, 0);
10572
+ if (snappedDay.getTime() !== inheritedDay.getTime()) {
10573
+ startDate = adapter.getClosestWorkTime({
10574
+ date: inheritedDay,
10575
+ dir: WORK_TIME_DIRECTION.FUTURE,
10576
+ task: base
10577
+ });
10578
+ }
10579
+ }
10278
10580
  const endDate = adapter.calculateEndDate({
10279
10581
  startDate,
10280
10582
  durationHours: base.durationHours,
@@ -10376,8 +10678,8 @@ function computeInsertionCorrelativeId(adapter, parentId, afterSiblingId, before
10376
10678
  }
10377
10679
  }
10378
10680
  if (siblings.length === 0) return isRoot ? 0 : 0;
10379
- const max = Math.max(...siblings.map(readCorrelativeId));
10380
- return max + 1;
10681
+ const max2 = Math.max(...siblings.map(readCorrelativeId));
10682
+ return max2 + 1;
10381
10683
  }
10382
10684
  var NOT_SET_SORT_VALUE2 = Number.MAX_SAFE_INTEGER;
10383
10685
  var TEMP_CID_SENTINEL = Number.MAX_SAFE_INTEGER - 1;
@@ -10387,12 +10689,7 @@ function readCorrelativeId(activity) {
10387
10689
  return Number.isFinite(n) ? n : NOT_SET_SORT_VALUE2;
10388
10690
  }
10389
10691
  function collectSiblings(adapter, parentKey) {
10390
- const siblings = [];
10391
- adapter.forEachActivity((a) => {
10392
- const ap = normalizeParentKey(a.parentId);
10393
- if (ap === parentKey) siblings.push(a);
10394
- });
10395
- return siblings;
10692
+ return collectChildrenSnapshots(parentKey, adapter);
10396
10693
  }
10397
10694
  function findFirstSelectedSibling(taskId, selected, adapter) {
10398
10695
  let firstSelected = taskId;
@@ -10442,7 +10739,7 @@ function computeFractionalCidAtIndex(adapter, parentKey, targetIndex, excludeId)
10442
10739
  // src/dispatch/handlers/activity-create.ts
10443
10740
  async function createActivityCore(action, deps, opts = {}) {
10444
10741
  const { adapter, scheduler, sector, calendars, activityIdGen, uidGen } = deps;
10445
- scheduler.invalidateAllCaches();
10742
+ if (!opts.skipCacheInvalidation) scheduler.invalidateAllCaches();
10446
10743
  const target = validateCreateTarget(action, adapter);
10447
10744
  if (!target.ok) return target;
10448
10745
  const { parent, anchorSiblingId, isRoot } = target;
@@ -10471,7 +10768,7 @@ async function createActivityCore(action, deps, opts = {}) {
10471
10768
  newActivity.progress
10472
10769
  );
10473
10770
  assignCustomId(newActivity, action, deps, anchorSiblingId, parent, opts);
10474
- const insertionCorrelativeId = computeInsertionCorrelativeId(
10771
+ const insertionCorrelativeId = opts.insertionCorrelativeId ?? computeInsertionCorrelativeId(
10475
10772
  adapter,
10476
10773
  isRoot ? ROOT_PARENT_ID : action.parentId,
10477
10774
  action.afterSiblingId,
@@ -10488,9 +10785,11 @@ async function createActivityCore(action, deps, opts = {}) {
10488
10785
  if (parentCustomId) {
10489
10786
  deps.customIdTracker.releaseCustomId(parentCustomId);
10490
10787
  }
10491
- recomputeHhCascadeForActivity(newId, adapter);
10492
- recomputeUsedCostCascadeForActivity(newId, adapter);
10493
- recomputeCostCascadeForActivity(newId, adapter);
10788
+ if (!opts.skipRollupCascades) {
10789
+ recomputeHhCascadeForActivity(newId, adapter);
10790
+ recomputeUsedCostCascadeForActivity(newId, adapter);
10791
+ recomputeCostCascadeForActivity(newId, adapter);
10792
+ }
10494
10793
  recomputeAndFoldCorrelatives(
10495
10794
  adapter,
10496
10795
  newId,
@@ -10662,296 +10961,62 @@ function findDefaultCalendarId(calendars) {
10662
10961
  return "global";
10663
10962
  }
10664
10963
 
10665
- // src/dispatch/handlers/activity-paste.ts
10666
- function resolvePasteRootDestination(destination, adapter) {
10667
- if ("afterSiblingId" in destination) {
10668
- const sibling = adapter.getActivity(destination.afterSiblingId);
10669
- if (!sibling) {
10670
- return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
10964
+ // src/propagations/upward/recompute-rollup-cascades.ts
10965
+ function recomputeRollupCascadesForParent(parentId, adapter) {
10966
+ recomputeHhCascadeForParent(parentId, adapter);
10967
+ recomputeUsedCostCascadeForParent(parentId, adapter);
10968
+ recomputeCostCascadeForParent(parentId, adapter);
10969
+ }
10970
+
10971
+ // src/propagations/shared/traversal-helpers.ts
10972
+ function collectDescendantIds(rootIds, adapter) {
10973
+ const out = /* @__PURE__ */ new Set();
10974
+ const stack = [];
10975
+ for (const id of rootIds) {
10976
+ const key = String(id);
10977
+ if (!out.has(key)) {
10978
+ out.add(key);
10979
+ stack.push(key);
10671
10980
  }
10672
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
10673
- return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
10674
10981
  }
10675
- const parentId = destination.parentId;
10676
- const siblings = collectSiblings(adapter, parentId).sort(
10677
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
10678
- );
10679
- const before = siblings[destination.index]?.id;
10680
- return { ok: true, parentId, firstBefore: before };
10982
+ while (stack.length > 0) {
10983
+ const current = stack.pop();
10984
+ const children = adapter.getChildren(current);
10985
+ for (const childId of children) {
10986
+ const key = String(childId);
10987
+ if (!out.has(key)) {
10988
+ out.add(key);
10989
+ stack.push(key);
10990
+ }
10991
+ }
10992
+ }
10993
+ return out;
10681
10994
  }
10682
- function applyPasteDomainSemantics(overrides) {
10683
- const { customId: _customId, ...content } = overrides;
10995
+ function collectIncidentLinkIds(activityIds, adapter) {
10996
+ const out = /* @__PURE__ */ new Set();
10997
+ for (const activityId of activityIds) {
10998
+ for (const linkId of adapter.getOutgoingLinkIds(activityId)) {
10999
+ out.add(String(linkId));
11000
+ }
11001
+ for (const linkId of adapter.getIncomingLinkIds(activityId)) {
11002
+ out.add(String(linkId));
11003
+ }
11004
+ }
11005
+ return [...out];
11006
+ }
11007
+
11008
+ // src/internal/hierarchy/parent-demotion.ts
11009
+ function restoredPromotionFields(restore) {
11010
+ if (!restore) return null;
10684
11011
  return {
10685
- ...content,
10686
- progress: 0,
10687
- ponderator: 0,
10688
- usedCost: 0,
10689
- workHours: 0,
10690
- isLookahead: false
10691
- };
10692
- }
10693
- async function dispatchActivityPaste(action, options, deps) {
10694
- const { adapter, scheduler, sector } = deps;
10695
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
10696
- if (!rootDest.ok) return rootDest;
10697
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
10698
- const touched = /* @__PURE__ */ new Set();
10699
- const originalToNew = /* @__PURE__ */ new Map();
10700
- const createdIds = [];
10701
- let prevRootId;
10702
- for (const input of action.activities) {
10703
- const mappedParent = originalToNew.get(input.originalParentId);
10704
- let parentId;
10705
- let afterSiblingId;
10706
- let beforeSiblingId;
10707
- if (mappedParent !== void 0) {
10708
- parentId = mappedParent;
10709
- } else {
10710
- parentId = rootDest.parentId;
10711
- if (prevRootId === void 0) {
10712
- afterSiblingId = rootDest.firstAfter;
10713
- beforeSiblingId = rootDest.firstBefore;
10714
- } else {
10715
- afterSiblingId = prevRootId;
10716
- }
10717
- }
10718
- const core = await createActivityCore(
10719
- {
10720
- parentId,
10721
- afterSiblingId,
10722
- beforeSiblingId,
10723
- ...input.activityId ? { activityId: input.activityId } : {},
10724
- overrides: applyPasteDomainSemantics(input.overrides),
10725
- eventSource: action.eventSource
10726
- },
10727
- deps,
10728
- {
10729
- skipCorrelativeRecompute: true,
10730
- skipBeforeSnap: true,
10731
- customIdReferenceId: action.referenceActivityId
10732
- }
10733
- );
10734
- if (!core.ok) return core;
10735
- if (input.baselineSnapshot !== void 0) {
10736
- const snapshot = input.baselineSnapshot;
10737
- adapter.setActivityField(
10738
- core.newId,
10739
- "baselineSnapshot",
10740
- snapshot ? {
10741
- ...snapshot,
10742
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
10743
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
10744
- } : null
10745
- );
10746
- }
10747
- originalToNew.set(input.originalId, core.newId);
10748
- createdIds.push(core.newId);
10749
- if (mappedParent === void 0) prevRootId = core.newId;
10750
- }
10751
- const correlativeShifts = recomputeCorrelativeIds(adapter);
10752
- const createdIdSet = new Set(createdIds.map(String));
10753
- foldCorrelativeShifts(
10754
- adapter,
10755
- correlativeShifts,
10756
- createdIdSet,
10757
- beforeSnap,
10758
- touched
10759
- );
10760
- for (const createdId of createdIds) touched.add(String(createdId));
10761
- const appliedLinks = [];
10762
- for (const link of action.links) {
10763
- const newSource = originalToNew.get(link.source);
10764
- const newTarget = originalToNew.get(link.target);
10765
- if (newSource === void 0 || newTarget === void 0) continue;
10766
- const op = {
10767
- kind: "create",
10768
- source: newSource,
10769
- target: newTarget,
10770
- type: link.type,
10771
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
10772
- };
10773
- const res = applyLinkOperation(op, {
10774
- port: adapter,
10775
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
10776
- });
10777
- appliedLinks.push({
10778
- op,
10779
- finalLinkId: res.applied ? res.linkId : null,
10780
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
10781
- });
10782
- }
10783
- const { scheduledIds } = await runPostMutation(
10784
- {
10785
- adapter,
10786
- scheduler,
10787
- sector: deps.sector,
10788
- defaultBaseCalendarId: deps.defaultBaseCalendarId
10789
- },
10790
- {
10791
- action,
10792
- autoscheduleFrom: createdIds[0] ?? null,
10793
- recomputeParentsFrom: createdIds,
10794
- now: deps.now,
10795
- options
10796
- }
10797
- );
10798
- const linkChanges = buildLinkChangesForBatch(
10799
- appliedLinks,
10800
- /* @__PURE__ */ new Map(),
10801
- adapter
10802
- );
10803
- return {
10804
- ok: true,
10805
- changes: await assembleChangeSet(adapter, {
10806
- source: action,
10807
- beforeSnap,
10808
- touchedIds: touched,
10809
- scheduledIds,
10810
- links: linkChanges,
10811
- hoursPerDay: sector.hoursPerDay
10812
- })
10813
- };
10814
- }
10815
-
10816
- // src/dispatch/handlers/sir-sync.ts
10817
- function dispatchSirSync(action, deps) {
10818
- const { adapter } = deps;
10819
- const id = action.activityId;
10820
- const snapshot = adapter.getActivity(id);
10821
- if (!snapshot) {
10822
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
10823
- }
10824
- const before = snapshot.pendingRequestIds ?? [];
10825
- const after = action.pendingRequests.map((r) => r.id);
10826
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
10827
- const change = {
10828
- id,
10829
- kind: "updated",
10830
- fields: { pendingRequestIds: { before, after } },
10831
- after: { id, pendingRequestIds: after }
10832
- };
10833
- return {
10834
- ok: true,
10835
- changes: {
10836
- source: action,
10837
- activities: [change],
10838
- links: [],
10839
- calendars: [],
10840
- trackingEvents: []
10841
- }
10842
- };
10843
- }
10844
-
10845
- // src/dispatch/handlers/activity-lookahead-sync.ts
10846
- function dispatchActivityLookaheadSync(action, deps) {
10847
- const ids = [...new Set(action.activityIds.map(String))];
10848
- const snapshots = ids.map((id) => deps.adapter.getActivity(id));
10849
- if (snapshots.some((snapshot) => !snapshot)) {
10850
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
10851
- }
10852
- const activities = [];
10853
- for (const [index, id] of ids.entries()) {
10854
- const snapshot = snapshots[index];
10855
- if (!snapshot) continue;
10856
- const fields = {};
10857
- if (snapshot.isLookahead !== action.isLookahead) {
10858
- fields.isLookahead = {
10859
- before: snapshot.isLookahead,
10860
- after: action.isLookahead
10861
- };
10862
- deps.adapter.setActivityField(id, "isLookahead", action.isLookahead);
10863
- }
10864
- if (snapshot.hasLookaheadTasks !== action.hasLookaheadTasks) {
10865
- fields.hasLookaheadTasks = {
10866
- before: snapshot.hasLookaheadTasks,
10867
- after: action.hasLookaheadTasks
10868
- };
10869
- deps.adapter.setActivityField(
10870
- id,
10871
- "hasLookaheadTasks",
10872
- action.hasLookaheadTasks
10873
- );
10874
- }
10875
- if (Object.keys(fields).length === 0) continue;
10876
- activities.push({
10877
- id,
10878
- kind: "updated",
10879
- fields,
10880
- after: {
10881
- ...snapshot,
10882
- isLookahead: action.isLookahead,
10883
- hasLookaheadTasks: action.hasLookaheadTasks
10884
- }
10885
- });
10886
- }
10887
- return {
10888
- ok: true,
10889
- changes: {
10890
- source: action,
10891
- activities,
10892
- links: [],
10893
- calendars: [],
10894
- trackingEvents: []
10895
- }
10896
- };
10897
- }
10898
-
10899
- // src/propagations/upward/recompute-rollup-cascades.ts
10900
- function recomputeRollupCascadesForParent(parentId, adapter) {
10901
- recomputeHhCascadeForParent(parentId, adapter);
10902
- recomputeUsedCostCascadeForParent(parentId, adapter);
10903
- recomputeCostCascadeForParent(parentId, adapter);
10904
- }
10905
-
10906
- // src/propagations/shared/traversal-helpers.ts
10907
- function collectDescendantIds(rootIds, adapter) {
10908
- const out = /* @__PURE__ */ new Set();
10909
- const stack = [];
10910
- for (const id of rootIds) {
10911
- const key = String(id);
10912
- if (!out.has(key)) {
10913
- out.add(key);
10914
- stack.push(key);
10915
- }
10916
- }
10917
- while (stack.length > 0) {
10918
- const current = stack.pop();
10919
- const children = adapter.getChildren(current);
10920
- for (const childId of children) {
10921
- const key = String(childId);
10922
- if (!out.has(key)) {
10923
- out.add(key);
10924
- stack.push(key);
10925
- }
10926
- }
10927
- }
10928
- return out;
10929
- }
10930
- function collectIncidentLinkIds(activityIds, adapter) {
10931
- const out = /* @__PURE__ */ new Set();
10932
- for (const activityId of activityIds) {
10933
- for (const linkId of adapter.getOutgoingLinkIds(activityId)) {
10934
- out.add(String(linkId));
10935
- }
10936
- for (const linkId of adapter.getIncomingLinkIds(activityId)) {
10937
- out.add(String(linkId));
10938
- }
10939
- }
10940
- return [...out];
10941
- }
10942
-
10943
- // src/internal/hierarchy/parent-demotion.ts
10944
- function restoredPromotionFields(restore) {
10945
- if (!restore) return null;
10946
- return {
10947
- type: restore.type,
10948
- startDate: new Date(restore.startDate),
10949
- endDate: new Date(restore.endDate),
10950
- durationHours: restore.durationHours,
10951
- expectedProgressBaseline: restore.expectedProgressBaseline,
10952
- constraintType: restore.constraintType,
10953
- constraintDate: restore.constraintDate ? new Date(restore.constraintDate) : null,
10954
- progress: restore.progress
11012
+ type: restore.type,
11013
+ startDate: new Date(restore.startDate),
11014
+ endDate: new Date(restore.endDate),
11015
+ durationHours: restore.durationHours,
11016
+ expectedProgressBaseline: restore.expectedProgressBaseline,
11017
+ constraintType: restore.constraintType,
11018
+ constraintDate: restore.constraintDate ? new Date(restore.constraintDate) : null,
11019
+ progress: restore.progress
10955
11020
  };
10956
11021
  }
10957
11022
  function canonicalEndDateField(parent, durationHours, computeEndDate) {
@@ -11194,7 +11259,647 @@ function buildLinkDeletions(beforeLinks) {
11194
11259
  after: null
11195
11260
  });
11196
11261
  }
11197
- return out;
11262
+ return out;
11263
+ }
11264
+
11265
+ // src/dispatch/handlers/activity-batch.ts
11266
+ async function dispatchActivityBatch(action, options, deps) {
11267
+ const validationError = validateBatch(action, deps);
11268
+ if (validationError) return { ok: false, reason: validationError };
11269
+ const plan = buildBatchPlan(action, deps);
11270
+ if (!plan.ok) return plan;
11271
+ const existingActivityIds = action.mode === "replace" ? new Set(deps.adapter.getAllIds().map((id) => String(id))) : /* @__PURE__ */ new Set();
11272
+ const beforeSnap = action.mode === "replace" ? await snapshotActivities(deps.adapter, existingActivityIds) : /* @__PURE__ */ new Map();
11273
+ const beforeLinks = action.mode === "replace" ? snapshotAllLinks(deps) : /* @__PURE__ */ new Map();
11274
+ const viewState = action.mode === "replace" ? captureDeletedViewState(deps.adapter, existingActivityIds) : [];
11275
+ deps.scheduler.invalidateAllCaches();
11276
+ const appliedLinks = [];
11277
+ if (action.mode === "replace") {
11278
+ for (const linkId of beforeLinks.keys()) {
11279
+ const deleted = applyLinkOperation(
11280
+ { kind: "delete", linkId },
11281
+ { port: deps.adapter }
11282
+ );
11283
+ if (!deleted.applied) {
11284
+ return {
11285
+ ok: false,
11286
+ reason: deleted.rejected ?? REJECTION_REASON.INVALID
11287
+ };
11288
+ }
11289
+ appliedLinks.push({
11290
+ op: { kind: "delete", linkId },
11291
+ finalLinkId: linkId,
11292
+ rejected: null
11293
+ });
11294
+ }
11295
+ for (const activityId of existingActivityIds) {
11296
+ const customId = deps.adapter.getActivity(activityId)?.customId;
11297
+ if (typeof customId === "string" && customId.length > 0) {
11298
+ deps.customIdTracker.releaseCustomId(customId);
11299
+ }
11300
+ deps.adapter.removeActivity(activityId);
11301
+ }
11302
+ }
11303
+ const created = await createPlannedActivities(action, plan.plan.items, deps);
11304
+ if (!created.ok) return created;
11305
+ const correlativeShifts = isSimpleRootAppend(action) ? [] : recomputeCorrelativeIds(deps.adapter);
11306
+ const createdIdSet = new Set(
11307
+ plan.plan.items.map((item) => String(item.activityId))
11308
+ );
11309
+ const existingCorrelativeShifts = correlativeShifts.filter(
11310
+ (shift) => !createdIdSet.has(shift.activityId)
11311
+ );
11312
+ const touchedIds = action.mode === "replace" ? new Set(existingActivityIds) : /* @__PURE__ */ new Set();
11313
+ foldCorrelativeShifts(
11314
+ deps.adapter,
11315
+ correlativeShifts,
11316
+ createdIdSet,
11317
+ beforeSnap,
11318
+ touchedIds
11319
+ );
11320
+ for (const item of plan.plan.items) {
11321
+ touchedIds.add(String(item.activityId));
11322
+ }
11323
+ const linkCreates = plan.plan.links.map((link) => ({
11324
+ operation: {
11325
+ kind: "create",
11326
+ source: link.source,
11327
+ target: link.target,
11328
+ type: link.type,
11329
+ lag: options.inputUnit === "hours" ? link.lag : lagDaysToHours(link.lag, deps.sector.hoursPerDay)
11330
+ },
11331
+ linkId: link.linkId
11332
+ }));
11333
+ const createdLinks = applyLinkCreates(linkCreates, {
11334
+ port: deps.adapter,
11335
+ preserved: beforeLinks
11336
+ });
11337
+ if (!createdLinks.applied) {
11338
+ return {
11339
+ ok: false,
11340
+ reason: createdLinks.rejected ?? REJECTION_REASON.INVALID
11341
+ };
11342
+ }
11343
+ for (const { operation, linkId } of linkCreates) {
11344
+ appliedLinks.push({
11345
+ op: operation,
11346
+ finalLinkId: linkId,
11347
+ rejected: null
11348
+ });
11349
+ }
11350
+ const createdIds = plan.plan.items.map((item) => item.activityId);
11351
+ const { scheduledIds } = await runPostMutation(
11352
+ {
11353
+ adapter: deps.adapter,
11354
+ scheduler: deps.scheduler,
11355
+ sector: deps.sector,
11356
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
11357
+ },
11358
+ {
11359
+ action,
11360
+ autoscheduleFrom: action.items.length > 0 || action.links.length > 0 ? "roots" : null,
11361
+ recomputeParentsFrom: createdIds,
11362
+ now: deps.now,
11363
+ options
11364
+ }
11365
+ );
11366
+ const changes = await assembleChangeSet(deps.adapter, {
11367
+ source: action,
11368
+ beforeSnap,
11369
+ touchedIds,
11370
+ scheduledIds,
11371
+ links: buildLinkChangesForBatch(appliedLinks, beforeLinks, deps.adapter),
11372
+ hoursPerDay: deps.sector.hoursPerDay,
11373
+ correlativeShifts: existingCorrelativeShifts,
11374
+ ...action.mode === "replace" ? { sirDetection: "after-diff" } : {}
11375
+ });
11376
+ return {
11377
+ ok: true,
11378
+ changes: viewState.length > 0 ? { ...changes, viewState } : changes,
11379
+ ...action.mode === "replace" ? {
11380
+ __beforeSnap: beforeSnap,
11381
+ __beforeLinks: stringKeyedLinks(beforeLinks)
11382
+ } : {}
11383
+ };
11384
+ }
11385
+ function validateBatch(action, deps) {
11386
+ if (action.mode !== "append" && action.mode !== "replace") {
11387
+ return REJECTION_REASON.INVALID;
11388
+ }
11389
+ const batchIds = /* @__PURE__ */ new Set();
11390
+ const explicitActivityIds = /* @__PURE__ */ new Set();
11391
+ for (const item of action.items) {
11392
+ if (item.batchId.length === 0 || batchIds.has(item.batchId)) {
11393
+ return REJECTION_REASON.INVALID;
11394
+ }
11395
+ batchIds.add(item.batchId);
11396
+ if (item.index !== void 0 && (!Number.isInteger(item.index) || item.index < 0)) {
11397
+ return REJECTION_REASON.INVALID;
11398
+ }
11399
+ if (item.activityId !== void 0) {
11400
+ const activityId = String(item.activityId);
11401
+ if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId) || action.mode === "append" && deps.adapter.getActivity(activityId) !== null) {
11402
+ return REJECTION_REASON.INVALID;
11403
+ }
11404
+ explicitActivityIds.add(activityId);
11405
+ }
11406
+ }
11407
+ for (const item of action.items) {
11408
+ if (!validReference(item.parent, true, action.mode, batchIds, deps)) {
11409
+ return REJECTION_REASON.PARENT_NOT_FOUND;
11410
+ }
11411
+ }
11412
+ if (hasParentCycle(action.items)) return REJECTION_REASON.INVALID;
11413
+ const explicitLinkIds = /* @__PURE__ */ new Set();
11414
+ for (const link of action.links) {
11415
+ if (!Object.values(LINK_TYPE).includes(link.type) || !Number.isFinite(link.lag)) {
11416
+ return !Number.isFinite(link.lag) ? REJECTION_REASON.INVALID_LINK_LAG : REJECTION_REASON.INVALID_LINK_TYPE;
11417
+ }
11418
+ if (!validReference(link.source, false, action.mode, batchIds, deps) || !validReference(link.target, false, action.mode, batchIds, deps)) {
11419
+ return REJECTION_REASON.ACTIVITY_NOT_FOUND;
11420
+ }
11421
+ if (link.linkId !== void 0) {
11422
+ const linkId = String(link.linkId);
11423
+ if (linkId.length === 0 || explicitLinkIds.has(linkId) || action.mode === "append" && deps.adapter.getLink(linkId) !== null) {
11424
+ return REJECTION_REASON.INVALID;
11425
+ }
11426
+ explicitLinkIds.add(linkId);
11427
+ }
11428
+ }
11429
+ return null;
11430
+ }
11431
+ function validReference(reference, allowRoot, mode, batchIds, deps) {
11432
+ if (typeof reference === "string") {
11433
+ if (reference === "0") return allowRoot;
11434
+ return mode === "append" && deps.adapter.getActivity(String(reference)) !== null;
11435
+ }
11436
+ return reference !== null && reference.batchId.length > 0 && batchIds.has(reference.batchId);
11437
+ }
11438
+ function hasParentCycle(items) {
11439
+ const parentByBatchId = /* @__PURE__ */ new Map();
11440
+ for (const item of items) {
11441
+ if (typeof item.parent !== "string") {
11442
+ parentByBatchId.set(item.batchId, item.parent.batchId);
11443
+ }
11444
+ }
11445
+ const verified = /* @__PURE__ */ new Set();
11446
+ for (const item of items) {
11447
+ const path = /* @__PURE__ */ new Set();
11448
+ let current = item.batchId;
11449
+ while (current !== void 0 && !verified.has(current)) {
11450
+ if (path.has(current)) return true;
11451
+ path.add(current);
11452
+ current = parentByBatchId.get(current);
11453
+ }
11454
+ for (const batchId of path) verified.add(batchId);
11455
+ }
11456
+ return false;
11457
+ }
11458
+ function buildBatchPlan(action, deps) {
11459
+ for (const item of action.items) {
11460
+ if (item.activityId !== void 0) {
11461
+ deps.activityIdGen.bump(item.activityId);
11462
+ }
11463
+ }
11464
+ const activityIdsByBatchId = /* @__PURE__ */ new Map();
11465
+ for (const item of action.items) {
11466
+ activityIdsByBatchId.set(
11467
+ item.batchId,
11468
+ item.activityId ?? deps.activityIdGen.next()
11469
+ );
11470
+ }
11471
+ for (const link of action.links) {
11472
+ if (link.linkId !== void 0) deps.linkIdGen.bump(link.linkId);
11473
+ }
11474
+ const linkIds = action.links.map(
11475
+ (link) => link.linkId ?? deps.linkIdGen.next()
11476
+ );
11477
+ const insertionCorrelativeIds = planInsertionCorrelatives(
11478
+ action,
11479
+ activityIdsByBatchId,
11480
+ deps
11481
+ );
11482
+ const items = [];
11483
+ for (const item of action.items) {
11484
+ const activityId = activityIdsByBatchId.get(item.batchId);
11485
+ const insertionCorrelativeId = insertionCorrelativeIds.get(item.batchId);
11486
+ if (activityId === void 0 || insertionCorrelativeId === void 0) {
11487
+ return { ok: false, reason: REJECTION_REASON.INVALID };
11488
+ }
11489
+ const parentBatchId = typeof item.parent === "string" ? null : item.parent.batchId;
11490
+ const parentId = typeof item.parent === "string" ? item.parent : activityIdsByBatchId.get(item.parent.batchId);
11491
+ if (parentId === void 0) {
11492
+ return { ok: false, reason: REJECTION_REASON.PARENT_NOT_FOUND };
11493
+ }
11494
+ items.push({
11495
+ input: item,
11496
+ activityId,
11497
+ parentId,
11498
+ parentBatchId,
11499
+ insertionCorrelativeId
11500
+ });
11501
+ }
11502
+ const links = [];
11503
+ for (const [index, link] of action.links.entries()) {
11504
+ const source = resolveActivityReference(link.source, activityIdsByBatchId);
11505
+ const target = resolveActivityReference(link.target, activityIdsByBatchId);
11506
+ const linkId = linkIds[index];
11507
+ if (source === null || target === null || linkId === void 0) {
11508
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11509
+ }
11510
+ links.push({
11511
+ source,
11512
+ target,
11513
+ type: link.type,
11514
+ lag: link.lag,
11515
+ linkId
11516
+ });
11517
+ }
11518
+ return { ok: true, plan: { items, links } };
11519
+ }
11520
+ function planInsertionCorrelatives(action, activityIdsByBatchId, deps) {
11521
+ if (isSimpleRootAppend(action)) {
11522
+ const firstCorrelativeId = deps.adapter.activityCount();
11523
+ return new Map(
11524
+ action.items.map((item, index) => [
11525
+ item.batchId,
11526
+ firstCorrelativeId + index
11527
+ ])
11528
+ );
11529
+ }
11530
+ const newIds = new Set(
11531
+ Array.from(activityIdsByBatchId.values(), (id) => String(id))
11532
+ );
11533
+ const plansByParent = /* @__PURE__ */ new Map();
11534
+ for (const item of action.items) {
11535
+ const activityId = activityIdsByBatchId.get(item.batchId);
11536
+ if (activityId === void 0) continue;
11537
+ const parentId = typeof item.parent === "string" ? item.parent : activityIdsByBatchId.get(item.parent.batchId);
11538
+ if (parentId === void 0) continue;
11539
+ const parentKey = String(parentId);
11540
+ let plan = plansByParent.get(parentKey);
11541
+ if (!plan) {
11542
+ const existing = action.mode === "replace" || typeof item.parent !== "string" && activityIdsByBatchId.has(item.parent.batchId) ? [] : collectSiblings(deps.adapter, parentKey).sort((left, right) => {
11543
+ const diff = readCorrelativeId(left) - readCorrelativeId(right);
11544
+ return diff || String(left.id).localeCompare(String(right.id));
11545
+ }).map((activity) => String(activity.id));
11546
+ plan = { existing, newIdsByTarget: /* @__PURE__ */ new Map() };
11547
+ plansByParent.set(parentKey, plan);
11548
+ }
11549
+ const target = item.index === void 0 ? plan.existing.length : Math.min(item.index, plan.existing.length);
11550
+ const idsAtTarget = plan.newIdsByTarget.get(target) ?? [];
11551
+ idsAtTarget.push(String(activityId));
11552
+ plan.newIdsByTarget.set(target, idsAtTarget);
11553
+ }
11554
+ const result = /* @__PURE__ */ new Map();
11555
+ const batchIdByActivityId = new Map(
11556
+ Array.from(activityIdsByBatchId, ([batchId, activityId]) => [
11557
+ String(activityId),
11558
+ batchId
11559
+ ])
11560
+ );
11561
+ for (const plan of plansByParent.values()) {
11562
+ const order = [];
11563
+ for (let target = 0; target <= plan.existing.length; target += 1) {
11564
+ for (const newId of plan.newIdsByTarget.get(target) ?? []) {
11565
+ order.push(newId);
11566
+ }
11567
+ const existingId = plan.existing[target];
11568
+ if (existingId !== void 0) order.push(existingId);
11569
+ }
11570
+ let index = 0;
11571
+ while (index < order.length) {
11572
+ if (!newIds.has(order[index])) {
11573
+ index += 1;
11574
+ continue;
11575
+ }
11576
+ const start = index;
11577
+ while (index < order.length && newIds.has(order[index])) index += 1;
11578
+ const count = index - start;
11579
+ const left = start > 0 ? activityCorrelative(order[start - 1], deps) : null;
11580
+ const right = index < order.length ? activityCorrelative(order[index], deps) : null;
11581
+ for (let offset = 0; offset < count; offset += 1) {
11582
+ const id = order[start + offset];
11583
+ const batchId = batchIdByActivityId.get(id);
11584
+ if (batchId === void 0) continue;
11585
+ result.set(
11586
+ batchId,
11587
+ interpolateCorrelative(left, right, offset + 1, count)
11588
+ );
11589
+ }
11590
+ }
11591
+ }
11592
+ return result;
11593
+ }
11594
+ function isSimpleRootAppend(action) {
11595
+ return action.mode === "append" && action.items.every(
11596
+ (item) => item.parent === "0" && item.index === void 0
11597
+ );
11598
+ }
11599
+ function activityCorrelative(activityId, deps) {
11600
+ const activity = deps.adapter.getActivity(activityId);
11601
+ return activity ? readCorrelativeId(activity) : 0;
11602
+ }
11603
+ function interpolateCorrelative(left, right, position, count) {
11604
+ if (left !== null && right !== null && right > left) {
11605
+ return left + (right - left) * position / (count + 1);
11606
+ }
11607
+ if (left !== null) return left + position;
11608
+ if (right !== null) return right - (count - position + 1);
11609
+ return position - 1;
11610
+ }
11611
+ async function createPlannedActivities(action, plannedItems, deps) {
11612
+ const childrenByParent = /* @__PURE__ */ new Map();
11613
+ const ready = [];
11614
+ for (const item of plannedItems) {
11615
+ if (item.parentBatchId === null) {
11616
+ ready.push(item);
11617
+ continue;
11618
+ }
11619
+ const children = childrenByParent.get(item.parentBatchId) ?? [];
11620
+ children.push(item);
11621
+ childrenByParent.set(item.parentBatchId, children);
11622
+ }
11623
+ for (let index = 0; index < ready.length; index += 1) {
11624
+ const item = ready[index];
11625
+ const core = await createActivityCore(
11626
+ {
11627
+ parentId: item.parentId,
11628
+ activityId: item.activityId,
11629
+ ...item.input.overrides === void 0 ? {} : { overrides: item.input.overrides },
11630
+ ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
11631
+ },
11632
+ deps,
11633
+ {
11634
+ skipCorrelativeRecompute: true,
11635
+ skipBeforeSnap: true,
11636
+ skipCacheInvalidation: true,
11637
+ skipRollupCascades: true,
11638
+ insertionCorrelativeId: item.insertionCorrelativeId
11639
+ }
11640
+ );
11641
+ if (!core.ok) return core;
11642
+ for (const child of childrenByParent.get(item.input.batchId) ?? []) {
11643
+ ready.push(child);
11644
+ }
11645
+ }
11646
+ if (ready.length !== plannedItems.length) {
11647
+ return { ok: false, reason: REJECTION_REASON.INVALID };
11648
+ }
11649
+ const recomputedParents = /* @__PURE__ */ new Set();
11650
+ for (let index = ready.length - 1; index >= 0; index -= 1) {
11651
+ const parentId = ready[index].parentId;
11652
+ if (parentId === "0" || recomputedParents.has(String(parentId))) continue;
11653
+ recomputedParents.add(String(parentId));
11654
+ recomputeRollupCascadesForParent(parentId, deps.adapter);
11655
+ }
11656
+ return { ok: true };
11657
+ }
11658
+ function resolveActivityReference(reference, activityIdsByBatchId) {
11659
+ if (typeof reference === "string") return reference;
11660
+ return activityIdsByBatchId.get(reference.batchId) ?? null;
11661
+ }
11662
+ function snapshotAllLinks(deps) {
11663
+ return new Map(
11664
+ deps.adapter.getAllLinks().map((link) => [String(link.id), { ...link }])
11665
+ );
11666
+ }
11667
+ function stringKeyedLinks(links) {
11668
+ return new Map(Array.from(links, ([linkId, link]) => [String(linkId), link]));
11669
+ }
11670
+
11671
+ // src/dispatch/handlers/activity-paste.ts
11672
+ function resolvePasteRootDestination(destination, adapter) {
11673
+ if ("afterSiblingId" in destination) {
11674
+ const sibling = adapter.getActivity(destination.afterSiblingId);
11675
+ if (!sibling) {
11676
+ return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
11677
+ }
11678
+ const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
11679
+ return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
11680
+ }
11681
+ const parentId = destination.parentId;
11682
+ const siblings = collectSiblings(adapter, parentId).sort(
11683
+ (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
11684
+ );
11685
+ const before = siblings[destination.index]?.id;
11686
+ return { ok: true, parentId, firstBefore: before };
11687
+ }
11688
+ function applyPasteDomainSemantics(overrides) {
11689
+ const { customId: _customId, ...content } = overrides;
11690
+ return {
11691
+ ...content,
11692
+ progress: 0,
11693
+ ponderator: 0,
11694
+ usedCost: 0,
11695
+ workHours: 0,
11696
+ isLookahead: false
11697
+ };
11698
+ }
11699
+ async function dispatchActivityPaste(action, options, deps) {
11700
+ const { adapter, scheduler, sector } = deps;
11701
+ const rootDest = resolvePasteRootDestination(action.destination, adapter);
11702
+ if (!rootDest.ok) return rootDest;
11703
+ const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
11704
+ const touched = /* @__PURE__ */ new Set();
11705
+ const originalToNew = /* @__PURE__ */ new Map();
11706
+ const createdIds = [];
11707
+ let prevRootId;
11708
+ for (const input of action.activities) {
11709
+ const mappedParent = originalToNew.get(input.originalParentId);
11710
+ let parentId;
11711
+ let afterSiblingId;
11712
+ let beforeSiblingId;
11713
+ if (mappedParent !== void 0) {
11714
+ parentId = mappedParent;
11715
+ } else {
11716
+ parentId = rootDest.parentId;
11717
+ if (prevRootId === void 0) {
11718
+ afterSiblingId = rootDest.firstAfter;
11719
+ beforeSiblingId = rootDest.firstBefore;
11720
+ } else {
11721
+ afterSiblingId = prevRootId;
11722
+ }
11723
+ }
11724
+ const core = await createActivityCore(
11725
+ {
11726
+ parentId,
11727
+ afterSiblingId,
11728
+ beforeSiblingId,
11729
+ ...input.activityId ? { activityId: input.activityId } : {},
11730
+ overrides: applyPasteDomainSemantics(input.overrides),
11731
+ eventSource: action.eventSource
11732
+ },
11733
+ deps,
11734
+ {
11735
+ skipCorrelativeRecompute: true,
11736
+ skipBeforeSnap: true,
11737
+ customIdReferenceId: action.referenceActivityId
11738
+ }
11739
+ );
11740
+ if (!core.ok) return core;
11741
+ if (input.baselineSnapshot !== void 0) {
11742
+ const snapshot = input.baselineSnapshot;
11743
+ adapter.setActivityField(
11744
+ core.newId,
11745
+ "baselineSnapshot",
11746
+ snapshot ? {
11747
+ ...snapshot,
11748
+ startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
11749
+ endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
11750
+ } : null
11751
+ );
11752
+ }
11753
+ originalToNew.set(input.originalId, core.newId);
11754
+ createdIds.push(core.newId);
11755
+ if (mappedParent === void 0) prevRootId = core.newId;
11756
+ }
11757
+ const correlativeShifts = recomputeCorrelativeIds(adapter);
11758
+ const createdIdSet = new Set(createdIds.map(String));
11759
+ foldCorrelativeShifts(
11760
+ adapter,
11761
+ correlativeShifts,
11762
+ createdIdSet,
11763
+ beforeSnap,
11764
+ touched
11765
+ );
11766
+ for (const createdId of createdIds) touched.add(String(createdId));
11767
+ const appliedLinks = [];
11768
+ for (const link of action.links) {
11769
+ const newSource = originalToNew.get(link.source);
11770
+ const newTarget = originalToNew.get(link.target);
11771
+ if (newSource === void 0 || newTarget === void 0) continue;
11772
+ const op = {
11773
+ kind: "create",
11774
+ source: newSource,
11775
+ target: newTarget,
11776
+ type: link.type,
11777
+ lag: lagDaysToHours(link.lag, sector.hoursPerDay)
11778
+ };
11779
+ const res = applyLinkOperation(op, {
11780
+ port: adapter,
11781
+ newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
11782
+ });
11783
+ appliedLinks.push({
11784
+ op,
11785
+ finalLinkId: res.applied ? res.linkId : null,
11786
+ rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
11787
+ });
11788
+ }
11789
+ const { scheduledIds } = await runPostMutation(
11790
+ {
11791
+ adapter,
11792
+ scheduler,
11793
+ sector: deps.sector,
11794
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
11795
+ },
11796
+ {
11797
+ action,
11798
+ autoscheduleFrom: createdIds[0] ?? null,
11799
+ recomputeParentsFrom: createdIds,
11800
+ now: deps.now,
11801
+ options
11802
+ }
11803
+ );
11804
+ const linkChanges = buildLinkChangesForBatch(
11805
+ appliedLinks,
11806
+ /* @__PURE__ */ new Map(),
11807
+ adapter
11808
+ );
11809
+ return {
11810
+ ok: true,
11811
+ changes: await assembleChangeSet(adapter, {
11812
+ source: action,
11813
+ beforeSnap,
11814
+ touchedIds: touched,
11815
+ scheduledIds,
11816
+ links: linkChanges,
11817
+ hoursPerDay: sector.hoursPerDay
11818
+ })
11819
+ };
11820
+ }
11821
+
11822
+ // src/dispatch/handlers/sir-sync.ts
11823
+ function dispatchSirSync(action, deps) {
11824
+ const { adapter } = deps;
11825
+ const id = action.activityId;
11826
+ const snapshot = adapter.getActivity(id);
11827
+ if (!snapshot) {
11828
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11829
+ }
11830
+ const before = snapshot.pendingRequestIds ?? [];
11831
+ const after = action.pendingRequests.map((r) => r.id);
11832
+ adapter.setActivityField(action.activityId, "pendingRequestIds", after);
11833
+ const change = {
11834
+ id,
11835
+ kind: "updated",
11836
+ fields: { pendingRequestIds: { before, after } },
11837
+ after: { id, pendingRequestIds: after }
11838
+ };
11839
+ return {
11840
+ ok: true,
11841
+ changes: {
11842
+ source: action,
11843
+ activities: [change],
11844
+ links: [],
11845
+ calendars: [],
11846
+ trackingEvents: []
11847
+ }
11848
+ };
11849
+ }
11850
+
11851
+ // src/dispatch/handlers/activity-lookahead-sync.ts
11852
+ function dispatchActivityLookaheadSync(action, deps) {
11853
+ const ids = [...new Set(action.activityIds.map(String))];
11854
+ const snapshots = ids.map((id) => deps.adapter.getActivity(id));
11855
+ if (snapshots.some((snapshot) => !snapshot)) {
11856
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11857
+ }
11858
+ const activities = [];
11859
+ for (const [index, id] of ids.entries()) {
11860
+ const snapshot = snapshots[index];
11861
+ if (!snapshot) continue;
11862
+ const fields = {};
11863
+ if (snapshot.isLookahead !== action.isLookahead) {
11864
+ fields.isLookahead = {
11865
+ before: snapshot.isLookahead,
11866
+ after: action.isLookahead
11867
+ };
11868
+ deps.adapter.setActivityField(id, "isLookahead", action.isLookahead);
11869
+ }
11870
+ if (snapshot.hasLookaheadTasks !== action.hasLookaheadTasks) {
11871
+ fields.hasLookaheadTasks = {
11872
+ before: snapshot.hasLookaheadTasks,
11873
+ after: action.hasLookaheadTasks
11874
+ };
11875
+ deps.adapter.setActivityField(
11876
+ id,
11877
+ "hasLookaheadTasks",
11878
+ action.hasLookaheadTasks
11879
+ );
11880
+ }
11881
+ if (Object.keys(fields).length === 0) continue;
11882
+ activities.push({
11883
+ id,
11884
+ kind: "updated",
11885
+ fields,
11886
+ after: {
11887
+ ...snapshot,
11888
+ isLookahead: action.isLookahead,
11889
+ hasLookaheadTasks: action.hasLookaheadTasks
11890
+ }
11891
+ });
11892
+ }
11893
+ return {
11894
+ ok: true,
11895
+ changes: {
11896
+ source: action,
11897
+ activities,
11898
+ links: [],
11899
+ calendars: [],
11900
+ trackingEvents: []
11901
+ }
11902
+ };
11198
11903
  }
11199
11904
 
11200
11905
  // src/internal/hierarchy/invalid-links.ts
@@ -12792,7 +13497,10 @@ async function dispatchStatusCriteriaSet(action, deps) {
12792
13497
  }
12793
13498
 
12794
13499
  // src/dispatch/dispatch.ts
12795
- async function dispatch(action, options, deps) {
13500
+ async function performDispatch(action, options, deps) {
13501
+ if (action.kind === "activity-batch") {
13502
+ return dispatchActivityBatch(action, options, deps);
13503
+ }
12796
13504
  if (action.kind === "persistence-acknowledge") {
12797
13505
  return dispatchPersistenceAcknowledge(action, deps);
12798
13506
  }
@@ -16026,6 +16734,7 @@ var ScheduleState = class {
16026
16734
  };
16027
16735
  _writeCapture = new WriteCapture();
16028
16736
  _viewState = new ViewStateStore();
16737
+ _filterProjection = null;
16029
16738
  _viewStateBefore = null;
16030
16739
  _lastStartDateBefore = null;
16031
16740
  _promotionSnapshotBefore = null;
@@ -16140,6 +16849,13 @@ var ScheduleState = class {
16140
16849
  setActiveFilter(nextFilter) {
16141
16850
  this._captureViewStateOnce();
16142
16851
  this._viewState.setActiveFilter(nextFilter);
16852
+ this._filterProjection = null;
16853
+ }
16854
+ getFilterProjection() {
16855
+ return this._filterProjection;
16856
+ }
16857
+ setFilterProjection(projection) {
16858
+ this._filterProjection = projection;
16143
16859
  }
16144
16860
  getActiveOrder() {
16145
16861
  return this._viewState.getActiveOrder();
@@ -16181,6 +16897,7 @@ var ScheduleState = class {
16181
16897
  }
16182
16898
  hydrateViewState(seed2) {
16183
16899
  this._viewState.hydrate(seed2);
16900
+ this._filterProjection = null;
16184
16901
  this.invalidateDerivedOrder();
16185
16902
  }
16186
16903
  _captureViewStateOnce() {
@@ -16381,6 +17098,7 @@ var ScheduleState = class {
16381
17098
  this._restoreActivityFields(before);
16382
17099
  if (this._viewStateBefore !== null) {
16383
17100
  this._viewState.restore(this._viewStateBefore);
17101
+ this._filterProjection = null;
16384
17102
  this.invalidateDerivedOrder();
16385
17103
  }
16386
17104
  if (this._promotionSnapshotBefore !== null) {
@@ -16590,8 +17308,10 @@ function buildHistoryChangeSet(changes) {
16590
17308
  ...entry,
16591
17309
  after: null
16592
17310
  }));
17311
+ const source = changes.source.kind === "activity-batch" ? { ...changes.source, items: [], links: [] } : changes.source;
16593
17312
  return cloneDomainValue({
16594
17313
  ...changes,
17314
+ source,
16595
17315
  activities: activitiesWithoutAfter,
16596
17316
  links: linksWithoutAfter
16597
17317
  });
@@ -16608,7 +17328,7 @@ function captureCreatedSnapshots(state, changeSet) {
16608
17328
  if (live) afterSnap.set(String(ch.id), structuredCloneActivity(live));
16609
17329
  }
16610
17330
  for (const ch of changeSet.links) {
16611
- if (ch.kind !== "created") continue;
17331
+ if (ch.kind === "deleted") continue;
16612
17332
  const live = state.getLink(String(ch.id));
16613
17333
  if (live) afterLinks.set(String(ch.id), { ...live });
16614
17334
  }
@@ -16831,6 +17551,17 @@ function applyChangeSetSide(state, entry, side) {
16831
17551
  for (const ch of cs.links) {
16832
17552
  if (ch.kind === "updated") {
16833
17553
  if (!ch.fields) continue;
17554
+ if (Object.hasOwn(ch.fields, "source") || Object.hasOwn(ch.fields, "target")) {
17555
+ const snapshot = side === "before" ? entry.beforeLinks?.get(String(ch.id)) : entry.afterLinks?.get(String(ch.id));
17556
+ if (!snapshot) {
17557
+ throw new Error(
17558
+ `${side === "before" ? "undo" : "redo"}: missing link snapshot for ${ch.id}`
17559
+ );
17560
+ }
17561
+ state.removeLink(String(ch.id));
17562
+ state.addLink({ ...snapshot });
17563
+ continue;
17564
+ }
16834
17565
  for (const [field, fv] of Object.entries(ch.fields)) {
16835
17566
  setLinkFieldDynamic(
16836
17567
  state,
@@ -17101,9 +17832,26 @@ function idArrayEquals(current, saved) {
17101
17832
  return currentIds.every((value, index) => value === savedIds[index]);
17102
17833
  }
17103
17834
 
17835
+ // src/boundary/save/link-changes.ts
17836
+ function linkChangedSince(baseline, current) {
17837
+ return baseline.source !== current.source || baseline.target !== current.target || baseline.lag !== current.lag || baseline.type !== current.type;
17838
+ }
17839
+ function checkNoUpdatedLinks(baseline, current) {
17840
+ const baselineById = new Map(
17841
+ baseline.map((link) => [String(link.id), link])
17842
+ );
17843
+ return current.filter((link) => {
17844
+ const base = baselineById.get(String(link.id));
17845
+ if (!base) return false;
17846
+ return linkChangedSince(base, link);
17847
+ });
17848
+ }
17849
+
17104
17850
  // src/boundary/save/save-tracker.ts
17105
17851
  var SaveTracker = class {
17106
17852
  initialized = false;
17853
+ dirtyActivityIds = /* @__PURE__ */ new Set();
17854
+ dirtyLinkIds = /* @__PURE__ */ new Set();
17107
17855
  linksAtLastSave = /* @__PURE__ */ new Map();
17108
17856
  activitiesAtLastSave = /* @__PURE__ */ new Map();
17109
17857
  init(activities, links) {
@@ -17113,10 +17861,16 @@ var SaveTracker = class {
17113
17861
  }
17114
17862
  snapshotActivities(activities) {
17115
17863
  this.activitiesAtLastSave.clear();
17864
+ this.dirtyActivityIds.clear();
17116
17865
  for (const activity of activities) {
17117
- if (activity.proplannerId == null) continue;
17866
+ const activityId = String(activity.id);
17867
+ if (activity.proplannerId == null) {
17868
+ this.dirtyActivityIds.add(activityId);
17869
+ continue;
17870
+ }
17871
+ const persistable = projectPersistableActivity(activity);
17118
17872
  this.activitiesAtLastSave.set(String(activity.id), {
17119
- persistable: projectPersistableActivity(activity),
17873
+ persistable,
17120
17874
  deletedSnapshot: {
17121
17875
  id: activity.id,
17122
17876
  proplannerId: activity.proplannerId,
@@ -17124,12 +17878,20 @@ var SaveTracker = class {
17124
17878
  hadBaseline: activity.baselineSnapshot !== null || activity.baselinePoints.length > 0
17125
17879
  }
17126
17880
  });
17881
+ if (activityChangedSince(activity, persistable)) {
17882
+ this.dirtyActivityIds.add(activityId);
17883
+ }
17127
17884
  }
17128
17885
  }
17129
17886
  snapshotLinks(links) {
17130
17887
  this.linksAtLastSave.clear();
17888
+ this.dirtyLinkIds.clear();
17131
17889
  for (const link of links) {
17132
- if (link.proplannerId == null) continue;
17890
+ const linkId = String(link.id);
17891
+ if (link.proplannerId == null) {
17892
+ this.dirtyLinkIds.add(linkId);
17893
+ continue;
17894
+ }
17133
17895
  this.linksAtLastSave.set(String(link.id), {
17134
17896
  id: link.id,
17135
17897
  proplannerId: link.proplannerId,
@@ -17140,21 +17902,52 @@ var SaveTracker = class {
17140
17902
  });
17141
17903
  }
17142
17904
  }
17143
- hasUnsavedChanges(activities, links) {
17144
- if (!this.initialized) return false;
17145
- if (this.deletedActivities(activities).length > 0) return true;
17146
- if (this.deletedLinks(links).length > 0) return true;
17147
- const activityChanged = activities.some((activity) => {
17148
- if (activity.proplannerId == null) return true;
17149
- const saved = this.activitiesAtLastSave.get(String(activity.id));
17150
- return !saved || activityChangedSince(activity, saved.persistable);
17151
- });
17152
- if (activityChanged) return true;
17153
- return links.some((link) => {
17154
- if (link.proplannerId == null) return true;
17155
- const saved = this.linksAtLastSave.get(String(link.id));
17156
- return !saved || link.lag !== saved.lag || link.type !== saved.type;
17157
- });
17905
+ recordChanges(activities, links, getActivity, getLink) {
17906
+ if (!this.initialized) return;
17907
+ for (const change of activities) {
17908
+ const activityId = String(change.id);
17909
+ const current = getActivity(activityId);
17910
+ const saved = this.activitiesAtLastSave.get(activityId);
17911
+ if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || activityChangedSince(current, saved.persistable)) {
17912
+ this.dirtyActivityIds.add(activityId);
17913
+ } else {
17914
+ this.dirtyActivityIds.delete(activityId);
17915
+ }
17916
+ }
17917
+ for (const change of links) {
17918
+ const linkId = String(change.id);
17919
+ const current = getLink(linkId);
17920
+ const saved = this.linksAtLastSave.get(linkId);
17921
+ if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || linkChangedSince(saved, current)) {
17922
+ this.dirtyLinkIds.add(linkId);
17923
+ } else {
17924
+ this.dirtyLinkIds.delete(linkId);
17925
+ }
17926
+ }
17927
+ }
17928
+ acknowledgePersistedLookahead(activityIds, getActivity) {
17929
+ if (!this.initialized) return;
17930
+ for (const activityId of activityIds) {
17931
+ const current = getActivity(activityId);
17932
+ const saved = this.activitiesAtLastSave.get(activityId);
17933
+ if (current === null || saved === void 0) continue;
17934
+ const persistable = {
17935
+ ...saved.persistable,
17936
+ isLookahead: current.isLookahead
17937
+ };
17938
+ this.activitiesAtLastSave.set(activityId, {
17939
+ ...saved,
17940
+ persistable
17941
+ });
17942
+ if (current.proplannerId == null || activityChangedSince(current, persistable)) {
17943
+ this.dirtyActivityIds.add(activityId);
17944
+ } else {
17945
+ this.dirtyActivityIds.delete(activityId);
17946
+ }
17947
+ }
17948
+ }
17949
+ hasUnsavedChanges() {
17950
+ return this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
17158
17951
  }
17159
17952
  deletedActivities(activities) {
17160
17953
  if (!this.initialized) return [];
@@ -17192,10 +17985,13 @@ var SaveTracker = class {
17192
17985
  if (link.proplannerId == null) return false;
17193
17986
  const saved = this.linksAtLastSave.get(String(link.id));
17194
17987
  if (!saved) return true;
17195
- return link.lag !== saved.lag || link.type !== saved.type;
17988
+ return linkChangedSince(saved, link);
17196
17989
  });
17197
17990
  }
17198
17991
  clear() {
17992
+ this.initialized = false;
17993
+ this.dirtyActivityIds.clear();
17994
+ this.dirtyLinkIds.clear();
17199
17995
  this.linksAtLastSave.clear();
17200
17996
  this.activitiesAtLastSave.clear();
17201
17997
  }
@@ -17360,7 +18156,7 @@ function parseActivity(raw, context) {
17360
18156
  context.hoursPerDay
17361
18157
  ),
17362
18158
  isCritical: Boolean(raw.is_critical),
17363
- freeSlackHours: raw.freeSlack,
18159
+ freeSlackHours: null,
17364
18160
  expectedProgress: null,
17365
18161
  expectedProgressBaseline: null,
17366
18162
  status: null,
@@ -17797,13 +18593,19 @@ function createActivityIdGenerator(options = {}) {
17797
18593
  const n = typeof id === "number" ? id : Number(id);
17798
18594
  if (!Number.isFinite(n)) return;
17799
18595
  raise(n);
18596
+ },
18597
+ snapshot() {
18598
+ return counter;
18599
+ },
18600
+ restore(snapshot) {
18601
+ counter = snapshot;
17800
18602
  }
17801
18603
  };
17802
18604
  }
17803
18605
 
17804
18606
  // src/generators/link-id-generator.ts
17805
18607
  function createLinkIdGenerator(options = {}) {
17806
- let counter = options.seed ?? Date.now();
18608
+ let counter = options.seed ?? 0;
17807
18609
  const raise = (value) => {
17808
18610
  if (value > counter) counter = value;
17809
18611
  };
@@ -17822,6 +18624,12 @@ function createLinkIdGenerator(options = {}) {
17822
18624
  const n = typeof id === "number" ? id : Number(id);
17823
18625
  if (!Number.isFinite(n)) return;
17824
18626
  raise(n);
18627
+ },
18628
+ snapshot() {
18629
+ return counter;
18630
+ },
18631
+ restore(snapshot) {
18632
+ counter = snapshot;
17825
18633
  }
17826
18634
  };
17827
18635
  }
@@ -17851,6 +18659,12 @@ function createUniqueCorrelativeIdGenerator(options = {}) {
17851
18659
  if (uid2 == null) return;
17852
18660
  const n = typeof uid2 === "number" ? uid2 : Number(uid2);
17853
18661
  raise(n);
18662
+ },
18663
+ snapshot() {
18664
+ return counter;
18665
+ },
18666
+ restore(snapshot) {
18667
+ counter = snapshot;
17854
18668
  }
17855
18669
  };
17856
18670
  }
@@ -17927,13 +18741,13 @@ function minSuffixFromArray(suffixes) {
17927
18741
  if (!suffixes || suffixes.length === 0) {
17928
18742
  throw new Error("minSuffixFromArray requires non-empty array");
17929
18743
  }
17930
- return suffixes.reduce((min, current) => minSuffix(min, current));
18744
+ return suffixes.reduce((min2, current) => minSuffix(min2, current));
17931
18745
  }
17932
18746
  function maxSuffixFromArray(suffixes) {
17933
18747
  if (!suffixes || suffixes.length === 0) {
17934
18748
  throw new Error("maxSuffixFromArray requires non-empty array");
17935
18749
  }
17936
- return suffixes.reduce((max, current) => maxSuffix(max, current));
18750
+ return suffixes.reduce((max2, current) => maxSuffix(max2, current));
17937
18751
  }
17938
18752
 
17939
18753
  // src/generators/custom-id/generation-strategies.ts
@@ -18553,7 +19367,8 @@ var SCHEDULE_CORE_STATUS = {
18553
19367
  };
18554
19368
  function initializeCore(input) {
18555
19369
  const clock = input.clock ?? null;
18556
- const loadNow = clock ? endOfLocalDay(clock()) : null;
19370
+ const initializedAt = clock?.() ?? null;
19371
+ const loadNow = initializedAt ? endOfLocalDay(initializedAt) : null;
18557
19372
  const reporter = input.reporter ?? noopReporter;
18558
19373
  const parsed2 = parseFromBackend(
18559
19374
  {
@@ -18584,6 +19399,7 @@ function initializeCore(input) {
18584
19399
  knownIds: parsed2.activities.map((a) => a.id)
18585
19400
  });
18586
19401
  const linkIdGen = createLinkIdGenerator({
19402
+ seed: initializedAt?.getTime() ?? 0,
18587
19403
  knownIds: parsed2.links.map((l) => l.id)
18588
19404
  });
18589
19405
  const uidGen = createUniqueCorrelativeIdGenerator({
@@ -18627,13 +19443,204 @@ function initializeCore(input) {
18627
19443
  };
18628
19444
  }
18629
19445
 
19446
+ // src/propagations/upward/date-gesture-preview.ts
19447
+ var emptyFormula = () => ({
19448
+ constantStart: null,
19449
+ variableStartOffset: null,
19450
+ constantEnd: null,
19451
+ variableEndOffset: null
19452
+ });
19453
+ var min = (left, right) => left === null ? right : Math.min(left, right);
19454
+ var max = (left, right) => left === null ? right : Math.max(left, right);
19455
+ function createDateGesturePreview(adapter, gesture, assertCurrent) {
19456
+ const participantIds = [
19457
+ ...new Set(
19458
+ gesture.kind === "move" ? gesture.activityIds.map(String) : [String(gesture.activityId)]
19459
+ )
19460
+ ];
19461
+ if (participantIds.length === 0) {
19462
+ throw new Error("[ScheduleCore] date gesture has no participants");
19463
+ }
19464
+ const formulas = buildParticipantFormulas(adapter, gesture, participantIds);
19465
+ addAncestorFormulas(adapter, participantIds, formulas);
19466
+ return {
19467
+ getDates(activityId, proposedEdgeTimeMs) {
19468
+ assertCurrent();
19469
+ if (!Number.isFinite(proposedEdgeTimeMs)) {
19470
+ throw new Error("[ScheduleCore] invalid date gesture edge");
19471
+ }
19472
+ const formula = formulas.get(String(activityId));
19473
+ if (!formula) return null;
19474
+ const startTimeMs = evaluateStart(formula, proposedEdgeTimeMs);
19475
+ const endTimeMs = evaluateEnd(formula, proposedEdgeTimeMs);
19476
+ if (startTimeMs === null || endTimeMs === null) {
19477
+ throw new Error(
19478
+ `[ScheduleCore] incomplete date gesture bounds for ${String(activityId)}`
19479
+ );
19480
+ }
19481
+ return { startTimeMs, endTimeMs };
19482
+ }
19483
+ };
19484
+ }
19485
+ function buildParticipantFormulas(adapter, gesture, participantIds) {
19486
+ const formulas = /* @__PURE__ */ new Map();
19487
+ const anchorId = gesture.kind === "move" ? String(gesture.anchorActivityId) : null;
19488
+ const anchor = anchorId === null ? null : requireLeafActivity(adapter, anchorId);
19489
+ if (anchorId !== null && !participantIds.includes(anchorId)) {
19490
+ throw new Error("[ScheduleCore] move anchor is not a participant");
19491
+ }
19492
+ for (const activityId of participantIds) {
19493
+ const activity = requireLeafActivity(adapter, activityId);
19494
+ if (gesture.kind === "move") {
19495
+ const anchorStart = anchor.startDate.getTime();
19496
+ formulas.set(activityId, {
19497
+ constantStart: null,
19498
+ variableStartOffset: activity.startDate.getTime() - anchorStart,
19499
+ constantEnd: null,
19500
+ variableEndOffset: activity.endDate.getTime() - anchorStart
19501
+ });
19502
+ } else if (gesture.kind === "resize-start") {
19503
+ formulas.set(activityId, {
19504
+ constantStart: null,
19505
+ variableStartOffset: 0,
19506
+ constantEnd: activity.endDate.getTime(),
19507
+ variableEndOffset: null
19508
+ });
19509
+ } else {
19510
+ formulas.set(activityId, {
19511
+ constantStart: activity.startDate.getTime(),
19512
+ variableStartOffset: null,
19513
+ constantEnd: null,
19514
+ variableEndOffset: 0
19515
+ });
19516
+ }
19517
+ }
19518
+ return formulas;
19519
+ }
19520
+ function addAncestorFormulas(adapter, participantIds, formulas) {
19521
+ const ancestors = findAncestorsOfDirty(adapter, new Set(participantIds));
19522
+ for (const parentId of ancestors) {
19523
+ const formula = emptyFormula();
19524
+ for (const childId of adapter.getChildren(parentId)) {
19525
+ const childFormula = formulas.get(childId);
19526
+ if (childFormula) {
19527
+ mergeFormula(formula, childFormula);
19528
+ continue;
19529
+ }
19530
+ const child = adapter.getActivity(childId);
19531
+ if (!child) {
19532
+ throw new Error(
19533
+ `[ScheduleCore] missing child ${childId} while previewing parent ${parentId}`
19534
+ );
19535
+ }
19536
+ formula.constantStart = min(
19537
+ formula.constantStart,
19538
+ child.startDate.getTime()
19539
+ );
19540
+ formula.constantEnd = max(formula.constantEnd, child.endDate.getTime());
19541
+ }
19542
+ formulas.set(parentId, formula);
19543
+ }
19544
+ }
19545
+ function requireLeafActivity(adapter, activityId) {
19546
+ const activity = adapter.getActivity(activityId);
19547
+ if (!activity) {
19548
+ throw new Error(
19549
+ `[ScheduleCore] missing date gesture activity ${activityId}`
19550
+ );
19551
+ }
19552
+ if (adapter.hasChildren(activityId)) {
19553
+ throw new Error(
19554
+ `[ScheduleCore] summary activity ${activityId} cannot be dragged directly`
19555
+ );
19556
+ }
19557
+ return activity;
19558
+ }
19559
+ function mergeFormula(target, source) {
19560
+ if (source.constantStart !== null) {
19561
+ target.constantStart = min(target.constantStart, source.constantStart);
19562
+ }
19563
+ if (source.variableStartOffset !== null) {
19564
+ target.variableStartOffset = min(
19565
+ target.variableStartOffset,
19566
+ source.variableStartOffset
19567
+ );
19568
+ }
19569
+ if (source.constantEnd !== null) {
19570
+ target.constantEnd = max(target.constantEnd, source.constantEnd);
19571
+ }
19572
+ if (source.variableEndOffset !== null) {
19573
+ target.variableEndOffset = max(
19574
+ target.variableEndOffset,
19575
+ source.variableEndOffset
19576
+ );
19577
+ }
19578
+ }
19579
+ function evaluateStart(formula, proposedEdgeTimeMs) {
19580
+ if (formula.variableStartOffset === null) return formula.constantStart;
19581
+ const variable = proposedEdgeTimeMs + formula.variableStartOffset;
19582
+ return formula.constantStart === null ? variable : Math.min(formula.constantStart, variable);
19583
+ }
19584
+ function evaluateEnd(formula, proposedEdgeTimeMs) {
19585
+ if (formula.variableEndOffset === null) return formula.constantEnd;
19586
+ const variable = proposedEdgeTimeMs + formula.variableEndOffset;
19587
+ return formula.constantEnd === null ? variable : Math.max(formula.constantEnd, variable);
19588
+ }
19589
+
18630
19590
  // src/init/schedule-core.ts
19591
+ function changesRequireFilterProjectionRebuild(changes) {
19592
+ for (const change of changes.activities) {
19593
+ if (change.kind === "deleted") return true;
19594
+ const hasMovedParent = change.kind === "updated" && change.fields !== void 0 && "parentId" in change.fields;
19595
+ if (hasMovedParent) return true;
19596
+ }
19597
+ return false;
19598
+ }
19599
+ function collectChangedActivities(changes, adapter) {
19600
+ const activities = [];
19601
+ for (const change of changes.activities) {
19602
+ const activity = adapter.getActivity(change.id);
19603
+ if (activity === null) return null;
19604
+ activities.push(activity);
19605
+ }
19606
+ return activities;
19607
+ }
19608
+ function isCurrentFilterProjection(projection, filter, context) {
19609
+ if (projection === null || projection.filter !== filter) return false;
19610
+ const hasCurrentHours = projection.context.hoursPerDay === context.hoursPerDay;
19611
+ const hasCurrentLocale = projection.context.locale === context.locale;
19612
+ return hasCurrentHours && hasCurrentLocale;
19613
+ }
18631
19614
  var ScheduleCore = class {
18632
19615
  _status = SCHEDULE_CORE_STATUS.READY;
18633
19616
  coreRuntime;
18634
19617
  _undo = new UndoRecorder();
18635
19618
  _opQueue = Promise.resolve();
18636
19619
  _scheduleRevision = 0;
19620
+ /**
19621
+ * Revision counting mutations that have been ADMITTED, whether or not they
19622
+ * have changed anything yet. `_scheduleRevision` counts the ones that
19623
+ * actually did.
19624
+ *
19625
+ * INVARIANT: `_pendingScheduleRevision >= _scheduleRevision`, and equality
19626
+ * means no mutation is in flight.
19627
+ *
19628
+ * The two exist separately because the useful instant and the knowable
19629
+ * instant are not the same one. A critical-path calculation becomes garbage
19630
+ * the moment the next mutation is admitted, but whether that mutation is
19631
+ * substantive is only knowable once its ChangeSet exists, which is hundreds
19632
+ * of milliseconds later at project scale. Measured live on a 12268-activity
19633
+ * project: the cancellation flag flipped 929 ms into a 930 ms calculation,
19634
+ * 812 into 812 and 785 into 785, because the job's apply block queues behind
19635
+ * the very dispatch whose completion cancels it. Cancellation and job end
19636
+ * were the same event, so every discarded job ran the whole calculation.
19637
+ *
19638
+ * Splitting the counter buys the early signal without moving the meaning of
19639
+ * `_scheduleRevision`, whose three readers (`isCriticalPathSettled`,
19640
+ * `createDateGesturePreview` and the job's own `isCurrent`) are all written
19641
+ * against "the state actually changed".
19642
+ */
19643
+ _pendingScheduleRevision = 0;
18637
19644
  _criticalPathRevision = -1;
18638
19645
  _activeCriticalPath = null;
18639
19646
  constructor(input) {
@@ -18686,6 +19693,16 @@ var ScheduleCore = class {
18686
19693
  this.assertReady();
18687
19694
  return readActivity(this.coreRuntime.state, id);
18688
19695
  }
19696
+ createDateGesturePreview(gesture) {
19697
+ this.assertReady();
19698
+ const revision = this._scheduleRevision;
19699
+ return createDateGesturePreview(this.coreRuntime.state, gesture, () => {
19700
+ this.assertReady();
19701
+ if (this._scheduleRevision !== revision) {
19702
+ throw new Error("[ScheduleCore] date gesture preview is stale");
19703
+ }
19704
+ });
19705
+ }
18689
19706
  getAllActivitiesView() {
18690
19707
  this.assertReady();
18691
19708
  return readAllActivities(this.coreRuntime.state);
@@ -18783,10 +19800,7 @@ var ScheduleCore = class {
18783
19800
  }
18784
19801
  hasUnsavedChanges() {
18785
19802
  this.assertReady();
18786
- return this._saveTracker.hasUnsavedChanges(
18787
- this.coreRuntime.state.getAllActivities(),
18788
- this.getAllLinksView()
18789
- );
19803
+ return this._saveTracker.hasUnsavedChanges();
18790
19804
  }
18791
19805
  getDeletedActivitiesSinceLastSave() {
18792
19806
  this.assertReady();
@@ -18804,10 +19818,90 @@ var ScheduleCore = class {
18804
19818
  );
18805
19819
  return workPromise;
18806
19820
  }
19821
+ /**
19822
+ * Marks a mutation as admitted and invalidates the calculation in flight.
19823
+ *
19824
+ * Called BEFORE `_enqueue`, which is the point of the whole thing: the wait
19825
+ * in the operation queue is part of the window a running calculation wastes,
19826
+ * and it is the longest part of it when several mutations are already
19827
+ * queued.
19828
+ *
19829
+ * `dispatchChangesSchedulingState` is a pure function of the action, so this
19830
+ * decision needs no state and cannot be wrong about the action's nature. What
19831
+ * it cannot know yet is whether the mutation will produce anything, which is
19832
+ * what `_settleScheduleMutation` reconciles afterwards.
19833
+ */
19834
+ _beginScheduleMutation(action) {
19835
+ if (!dispatchChangesSchedulingState(action)) return false;
19836
+ return this._admitScheduleMutation();
19837
+ }
19838
+ /** Undo and redo have no action to classify: reaching them IS the mutation. */
19839
+ _admitScheduleMutation() {
19840
+ this._pendingScheduleRevision++;
19841
+ if (this._activeCriticalPath) {
19842
+ this._activeCriticalPath.cancellation.cancelled = true;
19843
+ }
19844
+ return true;
19845
+ }
19846
+ /**
19847
+ * Closes a mutation admitted by `_beginScheduleMutation`, on EVERY exit path.
19848
+ *
19849
+ * `changedState` false means the mutation was admitted and produced nothing
19850
+ * (rejected, non-substantive, or rolled back). The pending revision walks
19851
+ * back so the invariant holds and `isCriticalPathSettled` keeps telling the
19852
+ * truth, and the calculation this mutation killed for nothing is re-armed.
19853
+ *
19854
+ * The re-arm goes through the queue and that is not incidental. Started
19855
+ * outside it, the calculation would run immediately while the next queued
19856
+ * mutation is still waiting, and that mutation would kill it again on
19857
+ * admission: a cancel-and-restart treadmill burning a fresh project-wide deep
19858
+ * copy per lap. Inside the queue the restart cannot happen until the queue
19859
+ * drains, so at most one calculation is armed per settled mutation. The burst
19860
+ * scenario will NOT catch a regression here, because there every dispatch
19861
+ * succeeds and this path is never taken.
19862
+ *
19863
+ * It re-arms through `recomputeCriticalPath` rather than enqueueing the job
19864
+ * directly, because the queue slot must NOT await the job: the job's own
19865
+ * apply block needs a later slot on this same queue, so awaiting it from
19866
+ * inside a slot deadlocks the core. `recomputeCriticalPath` already has the
19867
+ * shape that returns the job's promise out of the slot instead of awaiting
19868
+ * it, and it keeps `_criticalPathReady` pointing at the live calculation.
19869
+ */
19870
+ _settleScheduleMutation(admitted, changedState) {
19871
+ if (!admitted) return;
19872
+ if (changedState) return;
19873
+ this._pendingScheduleRevision = Math.max(
19874
+ this._pendingScheduleRevision - 1,
19875
+ this._scheduleRevision
19876
+ );
19877
+ if (this._pendingScheduleRevision !== this._scheduleRevision) return;
19878
+ if (this._criticalPathRevision === this._scheduleRevision) return;
19879
+ void this.recomputeCriticalPath().catch((error) => {
19880
+ this.coreRuntime.reporter.error(
19881
+ "[ScheduleCore] Critical Path re-arm after a barren mutation failed",
19882
+ error
19883
+ );
19884
+ });
19885
+ }
18807
19886
  async dispatch(action, options = {}) {
18808
- return this._enqueue(() => this._dispatchInner(action, options));
19887
+ const admitted = this._beginScheduleMutation(action);
19888
+ return this._enqueue(() => this._dispatchInner(action, options, admitted));
19889
+ }
19890
+ async applyActivityBatch(action, options = {}) {
19891
+ const admitted = this._beginScheduleMutation(action);
19892
+ return this._enqueue(() => this._dispatchInner(action, options, admitted));
19893
+ }
19894
+ async _dispatchInner(action, options = {}, admitted = false) {
19895
+ let changedState = false;
19896
+ try {
19897
+ const dispatched = await this._runDispatch(action, options);
19898
+ changedState = dispatched.changedState;
19899
+ return dispatched.result;
19900
+ } finally {
19901
+ this._settleScheduleMutation(admitted, changedState);
19902
+ }
18809
19903
  }
18810
- async _dispatchInner(action, options = {}) {
19904
+ async _runDispatch(action, options) {
18811
19905
  this.assertReady();
18812
19906
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
18813
19907
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
@@ -18815,11 +19909,16 @@ var ScheduleCore = class {
18815
19909
  inputUnit: options.inputUnit ?? "days"
18816
19910
  });
18817
19911
  const historyPolicy = getDispatchHistoryPolicy(action);
19912
+ const generatorSnapshot = [
19913
+ this.coreRuntime.activityIdGenerator.snapshot(),
19914
+ this.coreRuntime.linkIdGenerator.snapshot(),
19915
+ this.coreRuntime.uniqueCorrelativeIdGenerator.snapshot()
19916
+ ];
18818
19917
  this.coreRuntime.state.beginWriteCapture();
18819
19918
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
18820
19919
  let result;
18821
19920
  try {
18822
- result = await dispatch(action, options, {
19921
+ result = await performDispatch(action, options, {
18823
19922
  adapter: this.coreRuntime.state,
18824
19923
  ctx: pipelineContext,
18825
19924
  scheduler: this.coreRuntime.scheduler,
@@ -18835,9 +19934,9 @@ var ScheduleCore = class {
18835
19934
  )?.id ?? null,
18836
19935
  now: this.coreRuntime.clock ? endOfLocalDay(this.coreRuntime.clock()) : null
18837
19936
  });
18838
- if (!result.ok) this._rollback();
19937
+ if (!result.ok) this._rollback(generatorSnapshot);
18839
19938
  } catch (error) {
18840
- this._rollback();
19939
+ this._rollback(generatorSnapshot);
18841
19940
  throw error;
18842
19941
  } finally {
18843
19942
  this.coreRuntime.state.endWriteCapture();
@@ -18860,19 +19959,36 @@ var ScheduleCore = class {
18860
19959
  Date.now()
18861
19960
  );
18862
19961
  }
18863
- if (result.ok && historyPolicy === "clear-on-success") {
18864
- this._saveTracker.snapshotActivities(
18865
- this.coreRuntime.state.getAllActivities()
18866
- );
18867
- this._saveTracker.snapshotLinks(this.getAllLinksView());
18868
- this._undo.clear();
19962
+ if (result.ok) {
19963
+ if (historyPolicy === "clear-on-success") {
19964
+ this._saveTracker.snapshotActivities(
19965
+ this.coreRuntime.state.getAllActivities()
19966
+ );
19967
+ this._saveTracker.snapshotLinks(this.getAllLinksView());
19968
+ this._undo.clear();
19969
+ } else if (action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true) {
19970
+ this._saveTracker.acknowledgePersistedLookahead(
19971
+ action.activityIds,
19972
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
19973
+ );
19974
+ } else {
19975
+ this._saveTracker.recordChanges(
19976
+ result.changes.activities,
19977
+ result.changes.links,
19978
+ (activityId) => this.coreRuntime.state.getActivity(activityId),
19979
+ (linkId) => this.coreRuntime.state.getLink(linkId)
19980
+ );
19981
+ }
18869
19982
  }
18870
19983
  result = this._withReappliedViewState(action, result);
18871
- if (!result.ok) return result;
19984
+ if (!result.ok) return { result, changedState: false };
18872
19985
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
18873
- this._recordScheduleMutation(options.skipCriticalPath !== true);
19986
+ this._recordScheduleMutation(
19987
+ options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
19988
+ );
19989
+ return { result: toPublicDispatchResult(result), changedState: true };
18874
19990
  }
18875
- return toPublicDispatchResult(result);
19991
+ return { result: toPublicDispatchResult(result), changedState: false };
18876
19992
  }
18877
19993
  recomputeCriticalPath() {
18878
19994
  const operation = this._enqueue(async () => ({
@@ -18882,7 +19998,11 @@ var ScheduleCore = class {
18882
19998
  return operation;
18883
19999
  }
18884
20000
  isCriticalPathSettled() {
18885
- return this._criticalPathRevision === this._scheduleRevision && this._activeCriticalPath === null;
20001
+ return this._criticalPathRevision === this._scheduleRevision && // An admitted mutation has not landed yet, so whatever is applied now is
20002
+ // about to be stale. Without this term `whenCriticalPathSettled` would
20003
+ // spin: it would keep arming calculations that `isCurrent` kills on their
20004
+ // first check, each one paying the project-wide deep copy first.
20005
+ this._pendingScheduleRevision === this._scheduleRevision && this._activeCriticalPath === null;
18886
20006
  }
18887
20007
  async whenCriticalPathSettled() {
18888
20008
  while (!this.isCriticalPathSettled()) {
@@ -18890,7 +20010,11 @@ var ScheduleCore = class {
18890
20010
  }
18891
20011
  }
18892
20012
  _withReappliedViewState(action, result) {
18893
- if (!result.ok || !reappliesViewState(action)) return result;
20013
+ if (!result.ok) return result;
20014
+ if (action.kind === "visibility-set") {
20015
+ this.coreRuntime.state.setFilterProjection(null);
20016
+ }
20017
+ if (!reappliesViewState(action)) return result;
18894
20018
  const merged = this._reapplyViewState(result.changes);
18895
20019
  return merged === null ? result : { ...result, changes: merged };
18896
20020
  }
@@ -18911,11 +20035,44 @@ var ScheduleCore = class {
18911
20035
  */
18912
20036
  _reapplyViewState(changes) {
18913
20037
  const withFilter = this._reapplyActiveFilter(changes);
18914
- const withOrder = this._reapplyActiveOrder(withFilter ?? changes);
18915
- const sequenced = this._emitTouchedBranchOrder(
18916
- withOrder ?? withFilter ?? changes
20038
+ const sequenced = this._emitTouchedBranchOrder(withFilter ?? changes);
20039
+ const withOrder = this._reapplyActiveOrder(
20040
+ sequenced.changes ?? withFilter ?? changes,
20041
+ sequenced.touchedParents
20042
+ );
20043
+ const reapplied = withOrder ?? sequenced.changes ?? withFilter;
20044
+ const finalChanges = reapplied ?? changes;
20045
+ const createdIds = finalChanges.activities.filter((change) => change.kind === "created").map((change) => String(change.id));
20046
+ if (createdIds.length === 0) return reapplied;
20047
+ const createdAndAncestors = new Set(createdIds);
20048
+ const filterProjection = this.coreRuntime.state.getFilterProjection();
20049
+ for (const createdId of createdIds) {
20050
+ if (filterProjection !== null) {
20051
+ filterProjection.pinnedVisibleIds.add(createdId);
20052
+ }
20053
+ let activityId = createdId;
20054
+ while (activityId !== null && activityId !== ROOT_PARENT_ID) {
20055
+ createdAndAncestors.add(activityId);
20056
+ if (filterProjection !== null) {
20057
+ filterProjection.visibleIds.add(activityId);
20058
+ filterProjection.visibleReferenceCounts.set(
20059
+ activityId,
20060
+ (filterProjection.visibleReferenceCounts.get(activityId) ?? 0) + 1
20061
+ );
20062
+ }
20063
+ activityId = this.coreRuntime.state.getParentId(activityId);
20064
+ }
20065
+ }
20066
+ const createdVisibility = applyVisibleSet(
20067
+ this.coreRuntime.state,
20068
+ createdAndAncestors,
20069
+ createdAndAncestors
18917
20070
  );
18918
- return sequenced ?? withOrder ?? withFilter;
20071
+ if (createdVisibility.length === 0) return reapplied;
20072
+ return {
20073
+ ...finalChanges,
20074
+ viewState: [...finalChanges.viewState ?? [], ...createdVisibility]
20075
+ };
18919
20076
  }
18920
20077
  /**
18921
20078
  * Emits `order` for the branches this mutation resequenced, when no user order
@@ -18937,18 +20094,19 @@ var ScheduleCore = class {
18937
20094
  */
18938
20095
  _emitTouchedBranchOrder(changes) {
18939
20096
  const adapter = this.coreRuntime.state;
18940
- if (adapter.getActiveOrder() !== null) return null;
18941
- const touched = collectResequencedParents(
20097
+ const touchedParents = collectResequencedParents(
18942
20098
  changes,
18943
20099
  (activityId) => adapter.getParentId(activityId)
18944
20100
  );
18945
- if (touched.size === 0) return null;
18946
- const order = [...touched].map((parentId) => ({
18947
- parentId,
18948
- childIds: getChildrenInVisualOrder(parentId, adapter)
18949
- })).filter((branch) => branch.childIds.length > 1);
18950
- if (order.length === 0) return null;
18951
- return { ...changes, order };
20101
+ if (adapter.getActiveOrder() !== null) {
20102
+ return { changes: null, touchedParents };
20103
+ }
20104
+ if (touchedParents.size === 0) {
20105
+ return { changes: null, touchedParents };
20106
+ }
20107
+ const order = collectBranchOrderForParents(adapter, touchedParents);
20108
+ if (order.length === 0) return { changes: null, touchedParents };
20109
+ return { changes: { ...changes, order }, touchedParents };
18952
20110
  }
18953
20111
  /**
18954
20112
  * Re-sequences the grid after any mutation that could have changed a value the
@@ -18959,23 +20117,63 @@ var ScheduleCore = class {
18959
20117
  * Production only re-sorts after a bar drag; diverging from that is a
18960
20118
  * deliberate product decision, not an oversight.
18961
20119
  */
18962
- _reapplyActiveOrder(changes) {
20120
+ _reapplyActiveOrder(changes, resequencedParents) {
18963
20121
  const adapter = this.coreRuntime.state;
18964
20122
  if (adapter.getActiveOrder() === null) return null;
18965
20123
  adapter.invalidateDerivedOrder();
18966
- return { ...changes, order: collectBranchOrder(adapter) };
20124
+ const hasIndirectOrderChanges = changes.links.length > 0 || changes.calendars.length > 0;
20125
+ if (hasIndirectOrderChanges) {
20126
+ return { ...changes, order: collectBranchOrder(adapter) };
20127
+ }
20128
+ const affectedParents = new Set(resequencedParents);
20129
+ for (const change of changes.activities) {
20130
+ if (change.kind === "deleted") {
20131
+ return { ...changes, order: collectBranchOrder(adapter) };
20132
+ }
20133
+ const parentId = adapter.getParentId(change.id);
20134
+ affectedParents.add(
20135
+ parentId === null ? ROOT_PARENT_ID : String(parentId)
20136
+ );
20137
+ }
20138
+ const order = collectBranchOrderForParents(adapter, affectedParents);
20139
+ if (order.length === 0) return null;
20140
+ return { ...changes, order };
18967
20141
  }
18968
20142
  _reapplyActiveFilter(changes) {
18969
- const filter = this.coreRuntime.state.getActiveFilter();
18970
- if (filter === null) return null;
18971
20143
  const adapter = this.coreRuntime.state;
18972
- const visibleIds = evaluateVisibleIds({
18973
- activities: adapter.getAllActivities(),
18974
- parentOf: (activityId) => adapter.getParentId(activityId),
20144
+ const filter = adapter.getActiveFilter();
20145
+ if (filter === null) return null;
20146
+ const context = buildFilterContext(this.coreRuntime.sector.hoursPerDay);
20147
+ const projection = adapter.getFilterProjection();
20148
+ const projectionIsCurrent = isCurrentFilterProjection(
20149
+ projection,
18975
20150
  filter,
18976
- context: buildFilterContext(this.coreRuntime.sector.hoursPerDay)
18977
- });
18978
- const viewState = applyVisibleSet(adapter, visibleIds);
20151
+ context
20152
+ );
20153
+ const changedActivities = changesRequireFilterProjectionRebuild(changes) ? null : collectChangedActivities(changes, adapter);
20154
+ let affectedIds;
20155
+ let visibleIds;
20156
+ const needsProjectionRebuild = changedActivities === null || !projectionIsCurrent;
20157
+ if (needsProjectionRebuild) {
20158
+ const rebuiltProjection = buildFilterProjection({
20159
+ activities: adapter.getAllActivities(),
20160
+ parentOf: (activityId) => adapter.getParentId(activityId),
20161
+ filter,
20162
+ context,
20163
+ ...projectionIsCurrent ? { pinnedVisibleIds: projection.pinnedVisibleIds } : {}
20164
+ });
20165
+ adapter.setFilterProjection(rebuiltProjection);
20166
+ visibleIds = rebuiltProjection.visibleIds;
20167
+ affectedIds = adapter.getAllIds();
20168
+ } else {
20169
+ affectedIds = updateFilterProjection(
20170
+ projection,
20171
+ changedActivities,
20172
+ (activityId) => adapter.getParentId(activityId)
20173
+ );
20174
+ visibleIds = projection.visibleIds;
20175
+ }
20176
+ const viewState = applyVisibleSet(adapter, visibleIds, affectedIds);
18979
20177
  if (viewState.length === 0) return null;
18980
20178
  return {
18981
20179
  ...changes,
@@ -18984,6 +20182,10 @@ var ScheduleCore = class {
18984
20182
  }
18985
20183
  _recordScheduleMutation(criticalPathIsFresh) {
18986
20184
  this._scheduleRevision++;
20185
+ this._pendingScheduleRevision = Math.max(
20186
+ this._pendingScheduleRevision,
20187
+ this._scheduleRevision
20188
+ );
18987
20189
  if (this._activeCriticalPath) {
18988
20190
  this._activeCriticalPath.cancellation.cancelled = true;
18989
20191
  }
@@ -19014,23 +20216,30 @@ var ScheduleCore = class {
19014
20216
  this.coreRuntime.baseCalendars
19015
20217
  )
19016
20218
  );
19017
- const isCurrent = () => !cancellation.cancelled && this._status !== SCHEDULE_CORE_STATUS.DESTROYED && this._scheduleRevision === revision;
20219
+ 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
20220
+ // garbage. Without this term the calculation only learns it is obsolete
20221
+ // when the mutation finishes, which measured live is the same instant the
20222
+ // job itself ends.
20223
+ this._pendingScheduleRevision === this._scheduleRevision;
19018
20224
  const promise = (async () => {
19019
20225
  const calculatedFields = await computeCriticalPathFields(
19020
20226
  isolatedState,
19021
20227
  this.coreRuntime.sector.hoursPerDay,
19022
20228
  isCurrent
19023
20229
  );
19024
- if (!isCurrent()) return null;
19025
- const fieldsByActivity = calculatedFields ?? /* @__PURE__ */ new Map();
20230
+ if (!isCurrent() || calculatedFields === null) return null;
19026
20231
  return this._enqueue(async () => {
19027
20232
  if (!isCurrent()) return null;
19028
20233
  this.coreRuntime.state.beginWriteCapture();
19029
20234
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
19030
20235
  let changes;
19031
20236
  try {
19032
- for (const [activityId, fields] of fieldsByActivity) {
19033
- this.coreRuntime.state.setActivityFields(activityId, fields);
20237
+ for (const [activityId, fields] of calculatedFields) {
20238
+ setCriticalPathFieldsIfChanged(
20239
+ this.coreRuntime.state,
20240
+ activityId,
20241
+ fields
20242
+ );
19034
20243
  }
19035
20244
  changes = await assembleChangeSet(this.coreRuntime.state, {
19036
20245
  source: { kind: "init" },
@@ -19092,47 +20301,17 @@ var ScheduleCore = class {
19092
20301
  }
19093
20302
  return { changes };
19094
20303
  }
19095
- _recomputeAfterHistoryRestore() {
19096
- const state = this.coreRuntime.state;
19097
- recomputeAllProgressRollup(state);
19098
- recomputeCanonicalRealWork(state);
19099
- emitRealCost(state);
19100
- if (this.coreRuntime.clock) {
19101
- const now = endOfLocalDay(this.coreRuntime.clock());
19102
- applyExpectedProgressLive(state, now);
19103
- let hasActiveBaseline2 = false;
19104
- state.forEachActivity((activity) => {
19105
- hasActiveBaseline2 ||= getActiveBaseline(activity) !== null;
19106
- });
19107
- if (hasActiveBaseline2) {
19108
- runExpectedProgressBase(
19109
- state,
19110
- now,
19111
- this.coreRuntime.baseCalendars.find(
19112
- (calendar) => calendar.baseDefault
19113
- )?.id ?? null
19114
- );
19115
- }
19116
- applyStatusPass(state, this.coreRuntime.sector.statusCriteria);
19117
- }
19118
- }
19119
20304
  undo(options = {}) {
20305
+ const admitted = this._admitScheduleMutation();
20306
+ let changedState = false;
19120
20307
  const operation = this._enqueue(async () => {
19121
- this.assertReady();
19122
- const entry = this._undo.takeUndo();
19123
- if (!entry) return null;
19124
- applyUndo(this.coreRuntime.state, entry);
19125
- this._recomputeAfterHistoryRestore();
19126
- this._recordScheduleMutation(false);
19127
- if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
19128
- this._undo.pushRedo(entry);
19129
- this._undo.resetCoalesce();
19130
- const changes = buildInverseChangeSet(
19131
- this.coreRuntime.state,
19132
- entry,
19133
- "before"
19134
- );
19135
- return this._reapplyViewState(changes) ?? changes;
20308
+ try {
20309
+ return await this._runUndo(options, (changed) => {
20310
+ changedState = changed;
20311
+ });
20312
+ } finally {
20313
+ this._settleScheduleMutation(admitted, changedState);
20314
+ }
19136
20315
  });
19137
20316
  void operation.then(
19138
20317
  (changes) => {
@@ -19149,23 +20328,45 @@ var ScheduleCore = class {
19149
20328
  );
19150
20329
  return operation;
19151
20330
  }
19152
- redo(options = {}) {
19153
- const operation = this._enqueue(async () => {
20331
+ async _runUndo(options, markChanged) {
20332
+ {
19154
20333
  this.assertReady();
19155
- const entry = this._undo.takeRedo();
20334
+ if (options.expectedUndoDepth !== void 0 && this._undo.undoDepth() !== options.expectedUndoDepth) {
20335
+ return null;
20336
+ }
20337
+ const entry = this._undo.takeUndo();
19156
20338
  if (!entry) return null;
19157
- applyRedo(this.coreRuntime.state, entry);
19158
- this._recomputeAfterHistoryRestore();
20339
+ applyUndo(this.coreRuntime.state, entry);
19159
20340
  this._recordScheduleMutation(false);
20341
+ markChanged(true);
19160
20342
  if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
19161
- this._undo.pushUndo(entry);
20343
+ this._undo.pushRedo(entry);
19162
20344
  this._undo.resetCoalesce();
19163
20345
  const changes = buildInverseChangeSet(
19164
20346
  this.coreRuntime.state,
19165
20347
  entry,
19166
- "after"
20348
+ "before"
20349
+ );
20350
+ this._saveTracker.recordChanges(
20351
+ changes.activities,
20352
+ changes.links,
20353
+ (activityId) => this.coreRuntime.state.getActivity(activityId),
20354
+ (linkId) => this.coreRuntime.state.getLink(linkId)
19167
20355
  );
19168
20356
  return this._reapplyViewState(changes) ?? changes;
20357
+ }
20358
+ }
20359
+ redo(options = {}) {
20360
+ const admitted = this._admitScheduleMutation();
20361
+ let changedState = false;
20362
+ const operation = this._enqueue(async () => {
20363
+ try {
20364
+ return await this._runRedo((changed) => {
20365
+ changedState = changed;
20366
+ });
20367
+ } finally {
20368
+ this._settleScheduleMutation(admitted, changedState);
20369
+ }
19169
20370
  });
19170
20371
  void operation.then(
19171
20372
  (changes) => {
@@ -19182,6 +20383,29 @@ var ScheduleCore = class {
19182
20383
  );
19183
20384
  return operation;
19184
20385
  }
20386
+ async _runRedo(markChanged) {
20387
+ this.assertReady();
20388
+ const entry = this._undo.takeRedo();
20389
+ if (!entry) return null;
20390
+ applyRedo(this.coreRuntime.state, entry);
20391
+ this._recordScheduleMutation(false);
20392
+ markChanged(true);
20393
+ if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
20394
+ this._undo.pushUndo(entry);
20395
+ this._undo.resetCoalesce();
20396
+ const changes = buildInverseChangeSet(
20397
+ this.coreRuntime.state,
20398
+ entry,
20399
+ "after"
20400
+ );
20401
+ this._saveTracker.recordChanges(
20402
+ changes.activities,
20403
+ changes.links,
20404
+ (activityId) => this.coreRuntime.state.getActivity(activityId),
20405
+ (linkId) => this.coreRuntime.state.getLink(linkId)
20406
+ );
20407
+ return this._reapplyViewState(changes) ?? changes;
20408
+ }
19185
20409
  canUndo() {
19186
20410
  return this._undo.canUndo();
19187
20411
  }
@@ -19222,10 +20446,17 @@ var ScheduleCore = class {
19222
20446
  );
19223
20447
  }
19224
20448
  }
19225
- _rollback() {
20449
+ _rollback(generatorSnapshot) {
19226
20450
  try {
19227
20451
  this.coreRuntime.state.restoreFromCapture();
19228
20452
  this.coreRuntime.customIdTracker.rollbackCustomIdTransaction();
20453
+ if (generatorSnapshot) {
20454
+ this.coreRuntime.activityIdGenerator.restore(generatorSnapshot[0]);
20455
+ this.coreRuntime.linkIdGenerator.restore(generatorSnapshot[1]);
20456
+ this.coreRuntime.uniqueCorrelativeIdGenerator.restore(
20457
+ generatorSnapshot[2]
20458
+ );
20459
+ }
19229
20460
  } catch (restoreError) {
19230
20461
  this._status = SCHEDULE_CORE_STATUS.POISONED;
19231
20462
  this.coreRuntime.reporter.error(
@@ -19245,18 +20476,6 @@ function toPublicDispatchResult(result) {
19245
20476
  return publicResult;
19246
20477
  }
19247
20478
 
19248
- // src/boundary/save/link-changes.ts
19249
- function checkNoUpdatedLinks(baseline, current) {
19250
- const baselineById = new Map(
19251
- baseline.map((link) => [String(link.id), link])
19252
- );
19253
- return current.filter((link) => {
19254
- const base = baselineById.get(String(link.id));
19255
- if (!base) return false;
19256
- return base.lag !== link.lag || base.type !== link.type;
19257
- });
19258
- }
19259
-
19260
20479
  // src/boundary/save/unsaved.ts
19261
20480
  var getUnsavedActivities = (activities = []) => (activities ?? []).filter((activity) => !activity.proplannerId);
19262
20481
 
@@ -19340,6 +20559,6 @@ var CREATION_KIND = {
19340
20559
  ROOT: "root"
19341
20560
  };
19342
20561
 
19343
- export { ACTIVITY_TYPE, CALENDAR_UNIT, COLUMN, CONSTRAINT_TYPE2 as CONSTRAINT_TYPE, CONSTRAINT_TYPE_TO_LABEL, CREATION_KIND, DEFAULT_HOURS_PER_DAY, DISPATCH_ACTION_KIND, DISPATCH_ACTION_KINDS, DISPATCH_TRACK_EVENT, LINK_CODE_TO_TYPE, LINK_TYPE, LINK_TYPE_CODE, NEW_ACTIVITY_DEFAULTS, REJECTION_REASON, ROOT_PARENT_ID, ScheduleCore, WORK_TIME_DIRECTION, checkNoUpdatedLinks, computeExpectedProgress, expectedProgressFromBaseline, getActiveBaseline, getUnsavedActivities, isRootParent, normalizeParentKey, parseFromBackend, willRunCriticalPath, yieldToBrowser };
20562
+ export { ACTIVITY_TYPE, CALENDAR_UNIT, COLUMN, CONSTRAINT_TYPE2 as CONSTRAINT_TYPE, CONSTRAINT_TYPE_TO_LABEL, CREATION_KIND, DEFAULT_HOURS_PER_DAY, DISPATCH_ACTION_KIND, DISPATCH_ACTION_KINDS, DISPATCH_TRACK_EVENT, LINK_CODE_TO_TYPE, LINK_TYPE, LINK_TYPE_CODE, NEW_ACTIVITY_DEFAULTS, REJECTION_REASON, ROOT_PARENT_ID, SCHEDULE_CORE_STATUS, STATUS, ScheduleCore, WORK_TIME_DIRECTION, checkNoUpdatedLinks, computeExpectedProgress, expectedProgressFromBaseline, getActiveBaseline, getUnsavedActivities, isRootParent, normalizeParentKey, parseFromBackend, willRunCriticalPath, yieldToBrowser };
19344
20563
  //# sourceMappingURL=index.js.map
19345
20564
  //# sourceMappingURL=index.js.map