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