@outbuild-company/schedule-core 1.7.0 → 1.9.0

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
@@ -141,12 +141,11 @@ var REJECTION_REASON = {
141
141
  ANCHOR_SIBLING_NOT_FOUND: "anchor_sibling_not_found",
142
142
  ANCHOR_SIBLING_WRONG_PARENT: "anchor_sibling_wrong_parent",
143
143
  PARENT_FROZEN_BY_SIR: "parent_frozen_by_sir",
144
- SIR_REQUEST_NOT_PENDING: "sir_request_not_pending",
145
- PREVIEW_UNSUPPORTED_ACTION: "preview_unsupported_action",
146
144
  CANNOT_MOVE_INTO_OWN_DESCENDANT: "cannot_move_into_own_descendant",
147
145
  INDENT_NO_ELIGIBLE_SIBLING: "indent_no_eligible_sibling",
148
146
  OUTDENT_ROOT_LEVEL_NOT_EDITABLE: "outdent_root_level_not_editable",
149
147
  HIERARCHY_CYCLE: "hierarchy_cycle",
148
+ SANDBOX_PERSISTENCE_FORBIDDEN: "sandbox_persistence_forbidden",
150
149
  INVALID_PROGRESS_VALUE: "invalid_progress_value_must_be_0_or_100"
151
150
  };
152
151
 
@@ -421,6 +420,107 @@ function getFieldDescriptor(field) {
421
420
  return FIELD_REGISTRY[field] ?? null;
422
421
  }
423
422
 
423
+ // src/internal/state/view-state.ts
424
+ function isRelativePlacementPinValid(activityId, pin, parentOf) {
425
+ const branchParentId = String(pin.branchParentId);
426
+ const currentParentId = String(parentOf(activityId) ?? "0");
427
+ if (currentParentId !== branchParentId) return false;
428
+ const referenceId = String(pin.referenceId);
429
+ return referenceId === branchParentId || String(parentOf(referenceId) ?? "0") === branchParentId;
430
+ }
431
+ var ViewStateStore = class {
432
+ checkedSet = /* @__PURE__ */ new Set();
433
+ hiddenSet = /* @__PURE__ */ new Set();
434
+ activeFilter = null;
435
+ activeOrder = null;
436
+ relativePlacementPins = /* @__PURE__ */ new Map();
437
+ isChecked(activityId) {
438
+ return this.checkedSet.has(activityId);
439
+ }
440
+ isVisible(activityId) {
441
+ return !this.hiddenSet.has(activityId);
442
+ }
443
+ hiddenIds() {
444
+ return [...this.hiddenSet];
445
+ }
446
+ getActiveFilter() {
447
+ return this.activeFilter;
448
+ }
449
+ setActiveFilter(nextFilter) {
450
+ this.activeFilter = nextFilter;
451
+ }
452
+ getActiveOrder() {
453
+ return this.activeOrder;
454
+ }
455
+ setActiveOrder(nextOrder) {
456
+ this.activeOrder = nextOrder;
457
+ }
458
+ setRelativePlacementPin(activityId, pin) {
459
+ this.relativePlacementPins.set(String(activityId), {
460
+ referenceId: String(pin.referenceId),
461
+ branchParentId: String(pin.branchParentId),
462
+ side: pin.side
463
+ });
464
+ }
465
+ getRelativePlacementPins() {
466
+ return this.relativePlacementPins;
467
+ }
468
+ restoreRelativePlacementPins(pins) {
469
+ this.relativePlacementPins = new Map(pins);
470
+ }
471
+ checkedIds() {
472
+ return [...this.checkedSet];
473
+ }
474
+ checkedCount() {
475
+ return this.checkedSet.size;
476
+ }
477
+ setChecked(activityId, nextChecked) {
478
+ if (nextChecked === this.checkedSet.has(activityId)) return false;
479
+ if (nextChecked) this.checkedSet.add(activityId);
480
+ else this.checkedSet.delete(activityId);
481
+ return true;
482
+ }
483
+ setVisible(activityId, nextVisible) {
484
+ if (nextVisible === !this.hiddenSet.has(activityId)) return false;
485
+ if (nextVisible) this.hiddenSet.delete(activityId);
486
+ else this.hiddenSet.add(activityId);
487
+ return true;
488
+ }
489
+ forget(activityId) {
490
+ this.checkedSet.delete(activityId);
491
+ this.hiddenSet.delete(activityId);
492
+ this.relativePlacementPins.delete(activityId);
493
+ for (const [pinnedId, pin] of this.relativePlacementPins) {
494
+ if (pin.referenceId === activityId) {
495
+ this.relativePlacementPins.delete(pinnedId);
496
+ }
497
+ }
498
+ }
499
+ hydrate(seed2) {
500
+ this.checkedSet = new Set(seed2.checkedIds ?? []);
501
+ this.hiddenSet = new Set(seed2.hiddenIds ?? []);
502
+ this.activeFilter = null;
503
+ this.activeOrder = null;
504
+ this.relativePlacementPins = /* @__PURE__ */ new Map();
505
+ }
506
+ snapshot() {
507
+ return {
508
+ checked: new Set(this.checkedSet),
509
+ hidden: new Set(this.hiddenSet),
510
+ activeFilter: this.activeFilter,
511
+ activeOrder: this.activeOrder,
512
+ relativePlacementPins: new Map(this.relativePlacementPins)
513
+ };
514
+ }
515
+ restore(snapshot) {
516
+ this.checkedSet = new Set(snapshot.checked);
517
+ this.hiddenSet = new Set(snapshot.hidden);
518
+ this.activeFilter = snapshot.activeFilter;
519
+ this.activeOrder = snapshot.activeOrder;
520
+ this.relativePlacementPins = new Map(snapshot.relativePlacementPins);
521
+ }
522
+ };
523
+
424
524
  // src/internal/filter/evaluate-filter.ts
425
525
  function toTargetDate(value) {
426
526
  if (value instanceof Date) return value;
@@ -563,13 +663,8 @@ function seedFilterProjection(input) {
563
663
  );
564
664
  }
565
665
  const isMatch = matchesFilter(activity, filter, context);
566
- const isPinned = input.pinnedVisibleIds?.has(activityId) === true;
567
- visibleReferenceCounts.set(
568
- activityId,
569
- (isMatch ? 1 : 0) + (isPinned ? 1 : 0)
570
- );
666
+ visibleReferenceCounts.set(activityId, isMatch ? 1 : 0);
571
667
  if (isMatch) matchedIds.add(activityId);
572
- if (isPinned) pinnedVisibleIds.add(activityId);
573
668
  }
574
669
  return {
575
670
  matchedIds,
@@ -579,6 +674,29 @@ function seedFilterProjection(input) {
579
674
  remainingChildren
580
675
  };
581
676
  }
677
+ function applyRelativePlacementPins(seed2, pins) {
678
+ if (pins === void 0) return;
679
+ for (const [activityId, pin] of pins) {
680
+ const pinnedId = String(activityId);
681
+ const referenceId = String(pin.referenceId);
682
+ if (!seed2.parentIds.has(pinnedId) || !seed2.parentIds.has(referenceId) || !isRelativePlacementPinValid(
683
+ pinnedId,
684
+ pin,
685
+ (activityId2) => seed2.parentIds.get(activityId2) ?? null
686
+ ) || (seed2.visibleReferenceCounts.get(referenceId) ?? 0) === 0) {
687
+ continue;
688
+ }
689
+ seed2.pinnedVisibleIds.add(pinnedId);
690
+ let currentId = pinnedId;
691
+ while (currentId !== null) {
692
+ seed2.visibleReferenceCounts.set(
693
+ currentId,
694
+ (seed2.visibleReferenceCounts.get(currentId) ?? 0) + 1
695
+ );
696
+ currentId = seed2.parentIds.get(currentId) ?? null;
697
+ }
698
+ }
699
+ }
582
700
  function propagateVisibleReferenceCounts(seed2) {
583
701
  const { visibleReferenceCounts, parentIds, remainingChildren } = seed2;
584
702
  const pendingIds = [];
@@ -610,6 +728,7 @@ function collectVisibleIds(visibleReferenceCounts) {
610
728
  function buildFilterProjection(input) {
611
729
  const seed2 = seedFilterProjection(input);
612
730
  propagateVisibleReferenceCounts(seed2);
731
+ applyRelativePlacementPins(seed2, input.relativePlacementPins);
613
732
  const visibleIds = collectVisibleIds(seed2.visibleReferenceCounts);
614
733
  return {
615
734
  filter: input.filter,
@@ -797,7 +916,8 @@ function resolveFilter(adapter, filter, hoursPerDay) {
797
916
  activities,
798
917
  parentOf: (activityId) => adapter.getParentId(activityId),
799
918
  filter,
800
- context: buildFilterContext(hoursPerDay)
919
+ context: buildFilterContext(hoursPerDay),
920
+ relativePlacementPins: adapter.getRelativePlacementPins()
801
921
  });
802
922
  return { visibleIds: projection.visibleIds, projection };
803
923
  }
@@ -848,25 +968,118 @@ function normalizeParentKey(parent) {
848
968
 
849
969
  // src/internal/hierarchy/visual-order.ts
850
970
  var NOT_SET_SORT_VALUE = Number.MAX_SAFE_INTEGER;
851
- function getChildrenInVisualOrder(parentId, adapter) {
971
+ var EMPTY_RELATIVE_PLACEMENT_PINS = /* @__PURE__ */ new Map();
972
+ function buildRelativePlacementPinIndex(adapter) {
973
+ const index = /* @__PURE__ */ new Map();
974
+ for (const [activityId, pin] of adapter.getRelativePlacementPins?.() ?? []) {
975
+ const pinnedId = String(activityId);
976
+ const referenceId = String(pin.referenceId);
977
+ if (adapter.getActivity(pinnedId) === null || adapter.getActivity(referenceId) === null || !isRelativePlacementPinValid(
978
+ pinnedId,
979
+ pin,
980
+ (activityId2) => adapter.getParentId(activityId2)
981
+ )) {
982
+ continue;
983
+ }
984
+ const targetParentId = String(pin.branchParentId);
985
+ const branchPins = index.get(targetParentId) ?? /* @__PURE__ */ new Map();
986
+ branchPins.set(pinnedId, pin);
987
+ index.set(targetParentId, branchPins);
988
+ }
989
+ return index;
990
+ }
991
+ function getChildrenInVisualOrder(parentId, adapter, pinIndex = buildRelativePlacementPinIndex(adapter)) {
852
992
  const children = collectChildrenSnapshots(parentId, adapter);
853
993
  const userOrder = adapter.getOrderComparator?.() ?? null;
854
994
  children.sort(
855
995
  userOrder === null ? compareActivitiesInVisualOrder : (a, b) => userOrder(a, b) || compareActivitiesInVisualOrder(a, b)
856
996
  );
857
- return children.map((a) => String(a.id));
997
+ return applyRelativePlacementPins2(
998
+ children.map((activity) => String(activity.id)),
999
+ parentId,
1000
+ pinIndex.get(String(parentId)) ?? EMPTY_RELATIVE_PLACEMENT_PINS
1001
+ );
1002
+ }
1003
+ function applyRelativePlacementPins2(orderedIds, parentId, pins) {
1004
+ const order = buildLinkedActivityOrder(orderedIds);
1005
+ for (const [activityId, pin] of pins) {
1006
+ const pinnedId = String(activityId);
1007
+ const referenceId = String(pin.referenceId);
1008
+ const isFirstChild = referenceId === String(parentId);
1009
+ if (!order.previous.has(pinnedId) || !isFirstChild && (!order.previous.has(referenceId) || pinnedId === referenceId)) {
1010
+ continue;
1011
+ }
1012
+ detachActivity(order, pinnedId);
1013
+ if (isFirstChild) insertActivityFirst(order, pinnedId);
1014
+ else insertActivityBeside(order, pinnedId, referenceId, pin.side);
1015
+ }
1016
+ return materializeActivityOrder(order);
1017
+ }
1018
+ function buildLinkedActivityOrder(orderedIds) {
1019
+ const previous = /* @__PURE__ */ new Map();
1020
+ const next = /* @__PURE__ */ new Map();
1021
+ for (let index = 0; index < orderedIds.length; index++) {
1022
+ const activityId = orderedIds[index];
1023
+ if (activityId === void 0) continue;
1024
+ previous.set(activityId, orderedIds[index - 1] ?? null);
1025
+ next.set(activityId, orderedIds[index + 1] ?? null);
1026
+ }
1027
+ return { previous, next, head: orderedIds[0] ?? null };
1028
+ }
1029
+ function detachActivity(order, activityId) {
1030
+ const before = order.previous.get(activityId) ?? null;
1031
+ const after = order.next.get(activityId) ?? null;
1032
+ if (before === null) order.head = after;
1033
+ else order.next.set(before, after);
1034
+ if (after !== null) order.previous.set(after, before);
1035
+ }
1036
+ function insertActivityFirst(order, activityId) {
1037
+ order.previous.set(activityId, null);
1038
+ order.next.set(activityId, order.head);
1039
+ if (order.head !== null) order.previous.set(order.head, activityId);
1040
+ order.head = activityId;
1041
+ }
1042
+ function insertActivityBeside(order, activityId, referenceId, side) {
1043
+ if (side === "before") {
1044
+ const before = order.previous.get(referenceId) ?? null;
1045
+ order.previous.set(activityId, before);
1046
+ order.next.set(activityId, referenceId);
1047
+ order.previous.set(referenceId, activityId);
1048
+ if (before === null) order.head = activityId;
1049
+ else order.next.set(before, activityId);
1050
+ return;
1051
+ }
1052
+ const after = order.next.get(referenceId) ?? null;
1053
+ order.previous.set(activityId, referenceId);
1054
+ order.next.set(activityId, after);
1055
+ order.next.set(referenceId, activityId);
1056
+ if (after !== null) order.previous.set(after, activityId);
1057
+ }
1058
+ function materializeActivityOrder(order) {
1059
+ const result = [];
1060
+ let activityId = order.head;
1061
+ while (activityId !== null) {
1062
+ result.push(activityId);
1063
+ activityId = order.next.get(activityId) ?? null;
1064
+ }
1065
+ return result;
858
1066
  }
859
1067
  function iterateInVisualOrder(adapter) {
860
1068
  const result = [];
1069
+ const pinIndex = buildRelativePlacementPinIndex(adapter);
861
1070
  const visit = (activity) => {
862
1071
  result.push(activity);
863
- const childIds = getChildrenInVisualOrder(String(activity.id), adapter);
1072
+ const childIds = getChildrenInVisualOrder(
1073
+ String(activity.id),
1074
+ adapter,
1075
+ pinIndex
1076
+ );
864
1077
  for (const childId of childIds) {
865
1078
  const child = adapter.getActivity(childId);
866
1079
  if (child) visit(child);
867
1080
  }
868
1081
  };
869
- const rootIds = getChildrenInVisualOrder(ROOT_PARENT_ID, adapter);
1082
+ const rootIds = getChildrenInVisualOrder(ROOT_PARENT_ID, adapter, pinIndex);
870
1083
  for (const rootId of rootIds) {
871
1084
  const root = adapter.getActivity(rootId);
872
1085
  if (root) visit(root);
@@ -927,8 +1140,9 @@ function setHas(set, id) {
927
1140
  // src/dispatch/shared/collect-branch-order.ts
928
1141
  function collectBranchOrder(adapter) {
929
1142
  const branches = [];
1143
+ const pinIndex = buildRelativePlacementPinIndex(adapter);
930
1144
  const visit = (parentId) => {
931
- const childIds = getChildrenInVisualOrder(parentId, adapter);
1145
+ const childIds = getChildrenInVisualOrder(parentId, adapter, pinIndex);
932
1146
  if (childIds.length > 1) {
933
1147
  branches.push({ parentId, childIds });
934
1148
  }
@@ -941,8 +1155,9 @@ function collectBranchOrder(adapter) {
941
1155
  }
942
1156
  function collectBranchOrderForParents(adapter, parentIds) {
943
1157
  const branches = [];
1158
+ const pinIndex = buildRelativePlacementPinIndex(adapter);
944
1159
  for (const parentId of parentIds) {
945
- const childIds = getChildrenInVisualOrder(parentId, adapter);
1160
+ const childIds = getChildrenInVisualOrder(parentId, adapter, pinIndex);
946
1161
  if (childIds.length > 1) branches.push({ parentId, childIds });
947
1162
  }
948
1163
  return branches;
@@ -1793,6 +2008,8 @@ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag,
1793
2008
  target: targetId,
1794
2009
  type: originalLink.type,
1795
2010
  lag: originalLink.lag,
2011
+ freeSlackHours: null,
2012
+ isCritical: false,
1796
2013
  _sourceLag: sourceLag,
1797
2014
  _targetLag: targetLag,
1798
2015
  _trueLag: trueLag,
@@ -2870,7 +3087,9 @@ function applyLinkCreates(links, deps) {
2870
3087
  source: operation.source,
2871
3088
  target: operation.target,
2872
3089
  type: operation.type,
2873
- lag: operation.lag
3090
+ lag: operation.lag,
3091
+ freeSlackHours: null,
3092
+ isCritical: false
2874
3093
  }))
2875
3094
  ];
2876
3095
  const effectiveVerdict = validateTopologyCommit({
@@ -2889,6 +3108,8 @@ function applyLinkCreates(links, deps) {
2889
3108
  target: operation.target,
2890
3109
  type: operation.type,
2891
3110
  lag: operation.lag,
3111
+ freeSlackHours: null,
3112
+ isCritical: false,
2892
3113
  ...preserved?.ganttId === void 0 ? {} : { ganttId: preserved.ganttId },
2893
3114
  ...preserved?.sectorId === void 0 ? {} : { sectorId: preserved.sectorId },
2894
3115
  ...preserved?.proplannerId === void 0 ? {} : { proplannerId: preserved.proplannerId }
@@ -2931,7 +3152,9 @@ function applyCreate(op, deps) {
2931
3152
  source: op.source,
2932
3153
  target: op.target,
2933
3154
  type: op.type,
2934
- lag: op.lag
3155
+ lag: op.lag,
3156
+ freeSlackHours: null,
3157
+ isCritical: false
2935
3158
  };
2936
3159
  const effectiveVerdict = validateTopologyCommit({
2937
3160
  port,
@@ -3445,19 +3668,44 @@ async function recomputeParentsCooperatively(parents, adapter, progressMode, opt
3445
3668
  }
3446
3669
  function findAncestorsOfDirty(adapter, dirtyIds) {
3447
3670
  const ancestors = /* @__PURE__ */ new Set();
3448
- for (const id of dirtyIds) walkAncestors(id, adapter, ancestors);
3671
+ const parentById = /* @__PURE__ */ new Map();
3672
+ for (const id of dirtyIds) {
3673
+ walkAncestors(id, adapter, ancestors, parentById);
3674
+ }
3449
3675
  if (ancestors.size === 0) return [];
3450
3676
  const ids = Array.from(ancestors);
3451
- const depths = computeDepths(ids, adapter);
3677
+ const depths = computeKnownDepths(ids, parentById);
3452
3678
  return sortDeepestFirst(ids, depths);
3453
3679
  }
3454
- function walkAncestors(id, adapter, collector) {
3680
+ function walkAncestors(id, adapter, collector, parentById) {
3455
3681
  let current = adapter.getParentId(id);
3456
3682
  while (current && !isRootParent(current)) {
3457
3683
  if (collector.has(current)) return;
3458
3684
  collector.add(current);
3459
- current = adapter.getParentId(current);
3685
+ const parent = adapter.getParentId(current);
3686
+ parentById.set(current, parent);
3687
+ current = parent;
3688
+ }
3689
+ }
3690
+ function computeKnownDepths(ids, parentById) {
3691
+ const depths = /* @__PURE__ */ new Map();
3692
+ for (const id of ids) {
3693
+ if (depths.has(id)) continue;
3694
+ const path = [];
3695
+ const inPath = /* @__PURE__ */ new Set();
3696
+ let current = id;
3697
+ while (current && !isRootParent(current) && !depths.has(current) && !inPath.has(current)) {
3698
+ path.push(current);
3699
+ inPath.add(current);
3700
+ current = parentById.get(current) ?? null;
3701
+ }
3702
+ let depth = current ? depths.get(current) ?? 0 : 0;
3703
+ for (let index = path.length - 1; index >= 0; index -= 1) {
3704
+ depth += 1;
3705
+ depths.set(path[index], depth);
3706
+ }
3460
3707
  }
3708
+ return depths;
3461
3709
  }
3462
3710
  function aggregateChildren(childrenIds, adapter, includeRealWork) {
3463
3711
  let minStart = null;
@@ -3681,19 +3929,6 @@ function durationToHours(durationDays, hoursPerDay) {
3681
3929
  return Math.round(Math.ceil(durationDays * hoursPerDay * 60) / 60);
3682
3930
  }
3683
3931
 
3684
- // src/columns/shared/error-codes.ts
3685
- var PARSE_ERROR = {
3686
- NOT_A_NUMBER: "not_a_number",
3687
- // ISSUE-057 (CP6): la unidad de una duración se declara, no se adivina.
3688
- MISSING_INPUT_UNIT: "missing_input_unit",
3689
- // D-OWNER-5: una duración en días es entera; una fracción se rechaza sin
3690
- // redondear (el redondeo silencioso era la puerta del bug de las 320 horas).
3691
- FRACTIONAL_DAYS: "fractional_days"
3692
- };
3693
- var VALIDATION_REASON = {
3694
- UNCHANGED: "unchanged",
3695
- NEGATIVE_DURATION: "negative_duration"};
3696
-
3697
3932
  // src/columns/end-date/setter.ts
3698
3933
  var setEndDate = (value) => ({
3699
3934
  endDate: value
@@ -3795,6 +4030,10 @@ var POST_PROCESSOR = {
3795
4030
  RECORD_LAST_START_DATE: "recordLastStartDate"
3796
4031
  };
3797
4032
 
4033
+ // src/columns/shared/error-codes.ts
4034
+ var VALIDATION_REASON = {
4035
+ NEGATIVE_DURATION: "negative_duration"};
4036
+
3798
4037
  // src/columns/shared/constraint-validation.ts
3799
4038
  function checkConstraintViolation2(constraintType, constraintDate, projectedDate) {
3800
4039
  return checkConstraintViolation(constraintType, constraintDate, projectedDate);
@@ -3819,18 +4058,7 @@ var durationPipeline = {
3819
4058
  },
3820
4059
  parseInput(rawValue, _activity, ctx) {
3821
4060
  const numericValue = parseFloat(String(rawValue));
3822
- if (!Number.isFinite(numericValue)) {
3823
- return parseError(PARSE_ERROR.NOT_A_NUMBER);
3824
- }
3825
- if (ctx.inputUnit === void 0) {
3826
- return parseError(PARSE_ERROR.MISSING_INPUT_UNIT);
3827
- }
3828
- if (ctx.inputUnit === "hours") {
3829
- return parsed(numericValue);
3830
- }
3831
- if (!Number.isInteger(numericValue)) {
3832
- return parseError(PARSE_ERROR.FRACTIONAL_DAYS);
3833
- }
4061
+ if (!Number.isFinite(numericValue)) return parseError("not_a_number");
3834
4062
  return parsed(durationToHours(numericValue, ctx.hoursPerDay));
3835
4063
  },
3836
4064
  validate(_activity, oldValue, newValue) {
@@ -4074,8 +4302,8 @@ var costPipeline = {
4074
4302
  }
4075
4303
  return canEdit();
4076
4304
  },
4077
- parseInput(rawValue, activity) {
4078
- if (String(rawValue).trim() === "") return parsed(activity.cost);
4305
+ parseInput(rawValue) {
4306
+ if (String(rawValue).trim() === "") return parsed(0);
4079
4307
  const numericValue = parseFloat(String(rawValue));
4080
4308
  if (Number.isNaN(numericValue)) return parseError("not_a_number");
4081
4309
  return parsed(numericValue);
@@ -4124,8 +4352,8 @@ var usedCostPipeline = {
4124
4352
  }
4125
4353
  return canEdit();
4126
4354
  },
4127
- parseInput(rawValue, activity) {
4128
- if (String(rawValue).trim() === "") return parsed(activity.usedCost);
4355
+ parseInput(rawValue) {
4356
+ if (String(rawValue).trim() === "") return parsed(0);
4129
4357
  const numericValue = parseFloat(String(rawValue));
4130
4358
  if (Number.isNaN(numericValue)) return parseError("not_a_number");
4131
4359
  return parsed(numericValue);
@@ -4866,7 +5094,9 @@ var ACTIVITY_PROPERTY = {
4866
5094
  CALENDAR_DURATION: "calendarDuration"};
4867
5095
  var LINK_PROPERTY = {
4868
5096
  TYPE: "type",
4869
- LAG: "lag"};
5097
+ LAG: "lag",
5098
+ FREE_SLACK_HOURS: "freeSlackHours",
5099
+ IS_CRITICAL: "isCritical"};
4870
5100
 
4871
5101
  // src/internal/state/dynamic-writes.ts
4872
5102
  function setActivityFieldDynamic(state, activityId, field, value) {
@@ -5722,10 +5952,11 @@ function mutateTheDateToFutureOrPastInBaseRestriction(dateBaseToCalculate, restr
5722
5952
 
5723
5953
  // src/critical-path/legacy/base/parents-calculations/forward.js
5724
5954
  var CalculateForwardParentsWithLinks = class {
5725
- constructor(linkedActivitiesData, activity, gantt) {
5955
+ constructor(linkedActivitiesData, activity, gantt, linkCandidates) {
5726
5956
  this.linkedActivitiesData = linkedActivitiesData;
5727
5957
  this.activity = activity;
5728
5958
  this.gantt = gantt;
5959
+ this.linkCandidates = linkCandidates;
5729
5960
  this.activityCalendar = this.gantt.getCalendar(this.activity.calendar_id);
5730
5961
  if (!this.activityCalendar) {
5731
5962
  throw new Error(
@@ -5770,6 +6001,10 @@ var CalculateForwardParentsWithLinks = class {
5770
6001
  continue;
5771
6002
  }
5772
6003
  if (calculation) {
6004
+ this.linkCandidates?.set(linkedActivity.linkId, {
6005
+ targetId: String(this.activity.id),
6006
+ ...calculation
6007
+ });
5773
6008
  calculations.push(calculation);
5774
6009
  }
5775
6010
  }
@@ -6217,7 +6452,8 @@ function parentWithLinks(params) {
6217
6452
  linkDirection,
6218
6453
  calculations,
6219
6454
  gantt,
6220
- direction
6455
+ direction,
6456
+ linkCandidates
6221
6457
  } = params;
6222
6458
  const linksArray = parent[linkProperty];
6223
6459
  const parentData = extractParentData(parent);
@@ -6232,7 +6468,8 @@ function parentWithLinks(params) {
6232
6468
  calculation = new forward_default(
6233
6469
  linksWithActivityData,
6234
6470
  parent,
6235
- gantt
6471
+ gantt,
6472
+ linkCandidates
6236
6473
  );
6237
6474
  calculation = calculation.calculate();
6238
6475
  } else {
@@ -6277,6 +6514,7 @@ function getLinksWithActivityDataForward(linksArray, linkDirection, calculations
6277
6514
  return null;
6278
6515
  }
6279
6516
  return {
6517
+ linkId: String(linkData.id),
6280
6518
  es: calculationsOfLinkedActivity.es,
6281
6519
  ef: calculationsOfLinkedActivity.ef,
6282
6520
  linkLag: linkData.lag,
@@ -6377,6 +6615,7 @@ var CalculationOfParent = class {
6377
6615
  this.calculationOfParentsFromLinks = params.calculationOfParentsFromLinks;
6378
6616
  this.singleParents = params.singleParents;
6379
6617
  this.parentCalculationFromChildren = params.parentCalculationFromChildren;
6618
+ this.linkCandidates = params.linkCandidates;
6380
6619
  this.gantt = params.gantt;
6381
6620
  }
6382
6621
  handleParentWithLinks() {
@@ -6424,7 +6663,8 @@ var CalculationOfParent = class {
6424
6663
  linkDirection: this.linkDirection,
6425
6664
  gantt: this.gantt,
6426
6665
  calculations: this.calculations,
6427
- direction: this.direction
6666
+ direction: this.direction,
6667
+ linkCandidates: this.linkCandidates
6428
6668
  });
6429
6669
  this.calculationsOfParentThatImpactChildrens.set(activity.id, calculation);
6430
6670
  this.calculationOfParentsFromLinks.add(activity.id);
@@ -7073,6 +7313,7 @@ var CriticalPathHelpers = class {
7073
7313
  calculationOfParentsFromLinks: this.calculationOfParentsFromLinks,
7074
7314
  singleParents: this.singleParents,
7075
7315
  parentCalculationFromChildren: this.parentCalculationFromChildren,
7316
+ linkCandidates: this.linkCandidates,
7076
7317
  gantt: this.gantt
7077
7318
  });
7078
7319
  if (this.pendingParentsWithLinks.has(activity.id)) {
@@ -7140,6 +7381,7 @@ var LinksRulesCalculationsMethods = class {
7140
7381
  this.parentsWithLinks = calculationObject.parentsWithLinks;
7141
7382
  this.parentsCalculations = calculationObject.parentsCalculations;
7142
7383
  this.gantt = calculationObject.ganttInstance;
7384
+ this.linkCandidates = calculationObject.linkCandidates;
7143
7385
  }
7144
7386
  getEsAndEfFromPreviousCalculatedActivities(activity) {
7145
7387
  const isAlap = this.alapMap.has(activity);
@@ -7311,7 +7553,12 @@ var LinksConstraintCalculator = class extends LinksRulesCalculationsMethods_defa
7311
7553
  if (!linkData) {
7312
7554
  throw new Error("Link data not found", { cause: { linkId: link } });
7313
7555
  }
7314
- return this.calculateLink(linkData);
7556
+ const calculation = this.calculateLink(linkData);
7557
+ this.linkCandidates?.set(String(linkData.id), {
7558
+ targetId: String(this.referenceActivity.id),
7559
+ ...calculation
7560
+ });
7561
+ return calculation;
7315
7562
  });
7316
7563
  const parent = Number(this.referenceActivity.parent);
7317
7564
  if (this.parentsWithLinks.has(parent)) {
@@ -7577,6 +7824,7 @@ var ForwardPath = class extends generic_calculations_default {
7577
7824
  this.totalFloatSnltFnlt = /* @__PURE__ */ new Map();
7578
7825
  this.totalFloatForMsoMfo = /* @__PURE__ */ new Map();
7579
7826
  this.totalFloatForAsap = /* @__PURE__ */ new Map();
7827
+ this.linkCandidates = /* @__PURE__ */ new Map();
7580
7828
  this.calculationOfParentsFromLinks = /* @__PURE__ */ new Set();
7581
7829
  this.parentCalculationFromChildren = /* @__PURE__ */ new Set();
7582
7830
  this.forbiddenActivitiesLinkedToParentsWithLinks = /* @__PURE__ */ new Set();
@@ -7675,7 +7923,8 @@ var ForwardPath = class extends generic_calculations_default {
7675
7923
  constraint: constraintType,
7676
7924
  parentsWithLinks: this.pendingParentsWithLinks,
7677
7925
  parentsCalculations: this.calculationsOfParentThatImpactChildrens,
7678
- ganttInstance: this.gantt
7926
+ ganttInstance: this.gantt,
7927
+ linkCandidates: this.linkCandidates
7679
7928
  }).calculate();
7680
7929
  if (constraintType === CONSTRAINT_TYPES.ASAP) {
7681
7930
  this.totalFloatForAsap.set(activity, {
@@ -7693,7 +7942,8 @@ var ForwardPath = class extends generic_calculations_default {
7693
7942
  constraint: constraintType,
7694
7943
  parentsWithLinks: this.pendingParentsWithLinks,
7695
7944
  parentsCalculations: this.calculationsOfParentThatImpactChildrens,
7696
- ganttInstance: this.gantt
7945
+ ganttInstance: this.gantt,
7946
+ linkCandidates: this.linkCandidates
7697
7947
  }).calculate());
7698
7948
  }
7699
7949
  if (constraintType === CONSTRAINT_TYPES.ALAP) {
@@ -8730,7 +8980,6 @@ var BackwardPath = class extends generic_calculations_default {
8730
8980
  }
8731
8981
  }
8732
8982
  if ([CONSTRAINT_TYPES.SNLT].includes(activity.constraint_type)) {
8733
- const constraintDate = activity.constraint_date;
8734
8983
  const parentLinkCalcualation2 = this.calculationsOfParentThatImpactChildrens.get(parent);
8735
8984
  const lsToComparison = addDurationToDate(
8736
8985
  this.gantt.getCalendar(activity.calendar_id),
@@ -8738,6 +8987,7 @@ var BackwardPath = class extends generic_calculations_default {
8738
8987
  -activity.duration,
8739
8988
  activity
8740
8989
  );
8990
+ const constraintDate = activity.constraint_date ?? lsToComparison;
8741
8991
  ls = new Date(
8742
8992
  Math.min(constraintDate.getTime(), lsToComparison.getTime())
8743
8993
  );
@@ -8749,8 +8999,8 @@ var BackwardPath = class extends generic_calculations_default {
8749
8999
  );
8750
9000
  }
8751
9001
  if ([CONSTRAINT_TYPES.FNLT].includes(activity.constraint_type)) {
8752
- const constraintDate = activity.constraint_date;
8753
9002
  const parentLinkCalcualation2 = this.calculationsOfParentThatImpactChildrens.get(parent);
9003
+ const constraintDate = activity.constraint_date ?? parentLinkCalcualation2.lf;
8754
9004
  ls = new Date(
8755
9005
  Math.min(constraintDate.getTime(), parentLinkCalcualation2.lf.getTime())
8756
9006
  );
@@ -9134,18 +9384,20 @@ var FreeFloat = class {
9134
9384
  );
9135
9385
  }
9136
9386
  calculateFreeFloatForEndOfProject(constraint) {
9387
+ const calculation = constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap.get(this.activity.id) : this.forwardMap.get(this.activity.id);
9137
9388
  return this.parseHoursToDays(
9138
9389
  this.calculationWithCalendar(
9139
9390
  this.gantt.getSubtaskDates().end_date,
9140
- constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap.get(this.activity.id).lf : this.forwardMap.get(this.activity.id).ef
9391
+ constraint === CONSTRAINT_TYPES.ALAP ? calculation?.lf : calculation?.ef
9141
9392
  )
9142
9393
  );
9143
9394
  }
9144
9395
  calculateFreeFloatOfRestriction(constraint) {
9396
+ const calculation = this.forwardMap.get(this.activity.id);
9145
9397
  return this.parseHoursToDays(
9146
9398
  this.calculationWithCalendar(
9147
9399
  this.activity.constraint_date,
9148
- constraint === CONSTRAINT_TYPES.FNLT ? this.forwardMap.get(this.activity.id).ef : this.forwardMap.get(this.activity.id).es
9400
+ constraint === CONSTRAINT_TYPES.FNLT ? calculation?.ef : calculation?.es
9149
9401
  )
9150
9402
  );
9151
9403
  }
@@ -9163,111 +9415,33 @@ var FreeFloat = class {
9163
9415
  sucessorId,
9164
9416
  predecessorId
9165
9417
  }) {
9166
- const getPredecessorDate = ({
9167
- direction,
9168
- constraint: constraint2 = CONSTRAINT_TYPES.ASAP
9169
- }) => {
9170
- if (constraint2 === CONSTRAINT_TYPES.ALAP) {
9171
- return this.backwardMap.get(predecessorId)[direction];
9418
+ const successor = (isSuscessorAlap ? this.backwardMap : this.forwardMap).get(sucessorId);
9419
+ const predecessor = (constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap : this.forwardMap).get(predecessorId);
9420
+ const predecessorStart = constraint === CONSTRAINT_TYPES.ALAP ? predecessor?.ls : predecessor?.es;
9421
+ const predecessorFinish = constraint === CONSTRAINT_TYPES.ALAP ? predecessor?.lf : predecessor?.ef;
9422
+ return {
9423
+ fs: {
9424
+ sucessor: successor?.ls ?? successor?.es,
9425
+ predecessor: predecessorFinish
9426
+ },
9427
+ ss: {
9428
+ sucessor: successor?.ls ?? successor?.es,
9429
+ predecessor: predecessorStart
9430
+ },
9431
+ ff: {
9432
+ sucessor: successor?.lf ?? successor?.ef,
9433
+ predecessor: predecessorFinish
9434
+ },
9435
+ sf: {
9436
+ sucessor: successor?.lf ?? successor?.ef,
9437
+ predecessor: predecessorStart
9172
9438
  }
9173
- return this.forwardMap.get(predecessorId)[direction];
9174
9439
  };
9175
- if (constraint === CONSTRAINT_TYPES.ASAP || constraint === CONSTRAINT_TYPES.SNET || constraint === CONSTRAINT_TYPES.FNET || constraint === CONSTRAINT_TYPES.MSO || constraint === CONSTRAINT_TYPES.MFO) {
9176
- return {
9177
- fs: {
9178
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9179
- predecessor: getPredecessorDate({ direction: "ef" })
9180
- },
9181
- ss: {
9182
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9183
- predecessor: getPredecessorDate({ direction: "es" })
9184
- },
9185
- ff: {
9186
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9187
- predecessor: getPredecessorDate({ direction: "ef" })
9188
- },
9189
- sf: {
9190
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9191
- predecessor: getPredecessorDate({ direction: "es" })
9192
- }
9193
- };
9194
- }
9195
- if (constraint === CONSTRAINT_TYPES.ALAP) {
9196
- return {
9197
- fs: {
9198
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9199
- predecessor: getPredecessorDate({
9200
- constraint: CONSTRAINT_TYPES.ALAP,
9201
- direction: "lf"
9202
- })
9203
- },
9204
- ss: {
9205
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9206
- predecessor: getPredecessorDate({
9207
- constraint: CONSTRAINT_TYPES.ALAP,
9208
- direction: "ls"
9209
- })
9210
- },
9211
- ff: {
9212
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9213
- predecessor: getPredecessorDate({
9214
- constraint: CONSTRAINT_TYPES.ALAP,
9215
- direction: "lf"
9216
- })
9217
- },
9218
- sf: {
9219
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9220
- predecessor: getPredecessorDate({
9221
- constraint: CONSTRAINT_TYPES.ALAP,
9222
- direction: "ls"
9223
- })
9224
- }
9225
- };
9226
- }
9227
- if (constraint === CONSTRAINT_TYPES.SNLT) {
9228
- return {
9229
- fs: {
9230
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9231
- predecessor: getPredecessorDate({ direction: "ef" })
9232
- },
9233
- ss: {
9234
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9235
- predecessor: getPredecessorDate({ direction: "es" })
9236
- },
9237
- ff: {
9238
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9239
- predecessor: getPredecessorDate({ direction: "ef" })
9240
- },
9241
- sf: {
9242
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9243
- predecessor: getPredecessorDate({ direction: "es" })
9244
- }
9245
- };
9246
- }
9247
- if (constraint === CONSTRAINT_TYPES.FNLT) {
9248
- return {
9249
- fs: {
9250
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9251
- predecessor: getPredecessorDate({ direction: "ef" })
9252
- },
9253
- ss: {
9254
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
9255
- predecessor: getPredecessorDate({ direction: "es" })
9256
- },
9257
- ff: {
9258
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9259
- predecessor: getPredecessorDate({ direction: "ef" })
9260
- },
9261
- sf: {
9262
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
9263
- predecessor: getPredecessorDate({ direction: "es" })
9264
- }
9265
- };
9266
- }
9267
9440
  }
9268
9441
  calculateMsoMfo(constraint) {
9269
9442
  const CONSTRAINT_DATE2 = this.activity.constraint_date || /* @__PURE__ */ new Date();
9270
- const DATE_TO_CALCULATE = constraint === CONSTRAINT_TYPES.MSO ? this.forwardMap.get(this.activity.id).es : this.forwardMap.get(this.activity.id).ef;
9443
+ const calculation = this.forwardMap.get(this.activity.id);
9444
+ const DATE_TO_CALCULATE = constraint === CONSTRAINT_TYPES.MSO ? calculation?.es : calculation?.ef;
9271
9445
  const freeSlack = this.calculationWithCalendar(
9272
9446
  CONSTRAINT_DATE2,
9273
9447
  DATE_TO_CALCULATE
@@ -9281,6 +9455,9 @@ var FreeFloat = class {
9281
9455
  return filterValues.length ? Math.min(...filterValues) : 0;
9282
9456
  }
9283
9457
  calculationWithCalendar(dateOne, dateTwo) {
9458
+ if (!(dateOne instanceof Date) || !(dateTwo instanceof Date) || !Number.isFinite(dateOne.getTime()) || !Number.isFinite(dateTwo.getTime())) {
9459
+ return Number.NaN;
9460
+ }
9284
9461
  const key = `${dateTwo.getTime()}_${dateOne.getTime()}`;
9285
9462
  if (this.durationCache.has(key)) {
9286
9463
  return this.durationCache.get(key);
@@ -9294,7 +9471,7 @@ var FreeFloat = class {
9294
9471
  return result;
9295
9472
  }
9296
9473
  parseHoursToDays(date2) {
9297
- if (!date2) {
9474
+ if (date2 === void 0 || date2 === null) {
9298
9475
  return 0;
9299
9476
  }
9300
9477
  return date2 / this.gantt.config.hoursPerDay;
@@ -9360,6 +9537,7 @@ var CriticalPath = class {
9360
9537
  this.backwardPathCalculations = null;
9361
9538
  this.totalFloat = /* @__PURE__ */ new Map();
9362
9539
  this.freeFloat = /* @__PURE__ */ new Map();
9540
+ this.linkResidualHours = /* @__PURE__ */ new Map();
9363
9541
  this.currentCalculationId = 0;
9364
9542
  }
9365
9543
  getCurrentCalculationId = () => this.currentCalculationId;
@@ -9390,6 +9568,13 @@ var CriticalPath = class {
9390
9568
  if (!isCurrent()) return false;
9391
9569
  this.forwardPathCalculations = forwardPathCalculations;
9392
9570
  this.backwardPathCalculations = backwardPath.calculations;
9571
+ const linkResidualHours = await this.calculateLinkResidualHours(
9572
+ forwardPath.linkCandidates,
9573
+ forwardPathCalculations,
9574
+ isCurrent
9575
+ );
9576
+ if (!linkResidualHours || !isCurrent()) return false;
9577
+ this.linkResidualHours = linkResidualHours;
9393
9578
  const { totalFloatMap, freeFloatMap } = await this.calculateFloatsAsync(
9394
9579
  forwardPath,
9395
9580
  backwardPath,
@@ -9409,6 +9594,38 @@ var CriticalPath = class {
9409
9594
  throw error;
9410
9595
  }
9411
9596
  }
9597
+ async calculateLinkResidualHours(linkCandidates, forwardCalculations, isCurrent = () => true) {
9598
+ const residuals = /* @__PURE__ */ new Map();
9599
+ let sliceStartTime = performance.now();
9600
+ for (const [linkId, candidate] of linkCandidates) {
9601
+ sliceStartTime = await yieldIfNeeded2(sliceStartTime);
9602
+ if (!isCurrent()) return null;
9603
+ const targetId = Number(candidate.targetId);
9604
+ const target = this.gantt.getTask(targetId);
9605
+ const settled = forwardCalculations.get(targetId);
9606
+ const candidateEf = candidate.ef;
9607
+ const settledEf = settled?.ef;
9608
+ const calendar = target && this.gantt.getCalendar(target.calendar_id);
9609
+ if (!calendar || !(candidateEf instanceof Date) || !(settledEf instanceof Date) || !Number.isFinite(candidateEf.getTime()) || !Number.isFinite(settledEf.getTime())) {
9610
+ residuals.set(linkId, null);
9611
+ continue;
9612
+ }
9613
+ const candidateTime = candidateEf.getTime();
9614
+ const settledTime = settledEf.getTime();
9615
+ const duration = settledTime >= candidateTime ? calendar.calculateDuration({
9616
+ start_date: candidateEf,
9617
+ end_date: settledEf
9618
+ }) : -calendar.calculateDuration({
9619
+ start_date: settledEf,
9620
+ end_date: candidateEf
9621
+ });
9622
+ residuals.set(
9623
+ linkId,
9624
+ Number.isFinite(duration) ? Object.is(duration, -0) ? 0 : duration : null
9625
+ );
9626
+ }
9627
+ return residuals;
9628
+ }
9412
9629
  async calculateFloatsAsync(forward, backward, isCurrent) {
9413
9630
  try {
9414
9631
  const projectDates = this.gantt.getSubtaskDates();
@@ -9512,14 +9729,52 @@ function computeLevels(state) {
9512
9729
  for (const id of activitiesById.keys()) resolveLevel(id, /* @__PURE__ */ new Set());
9513
9730
  return levelById;
9514
9731
  }
9515
- function toLegacyTasks(state) {
9732
+ function createLegacyIds(state) {
9733
+ const canonicalIds = /* @__PURE__ */ new Set();
9734
+ state.forEachActivity((activity, id) => {
9735
+ canonicalIds.add(String(id));
9736
+ if (activity.parentId !== null) canonicalIds.add(String(activity.parentId));
9737
+ });
9738
+ const numericCounts = /* @__PURE__ */ new Map();
9739
+ for (const id of canonicalIds) {
9740
+ const numeric = Number(id);
9741
+ if (Number.isSafeInteger(numeric) && numeric !== 0) {
9742
+ numericCounts.set(numeric, (numericCounts.get(numeric) ?? 0) + 1);
9743
+ }
9744
+ }
9745
+ const canonicalToLegacy = /* @__PURE__ */ new Map();
9746
+ const legacyToCanonical = /* @__PURE__ */ new Map();
9747
+ for (const id of canonicalIds) {
9748
+ const numeric = Number(id);
9749
+ if (Number.isSafeInteger(numeric) && numeric !== 0 && numericCounts.get(numeric) === 1) {
9750
+ canonicalToLegacy.set(id, numeric);
9751
+ legacyToCanonical.set(numeric, id);
9752
+ }
9753
+ }
9754
+ let next = 1;
9755
+ const toLegacy = (rawId) => {
9756
+ const id = String(rawId);
9757
+ const existing = canonicalToLegacy.get(id);
9758
+ if (existing !== void 0) return existing;
9759
+ while (legacyToCanonical.has(next)) next += 1;
9760
+ canonicalToLegacy.set(id, next);
9761
+ legacyToCanonical.set(next, id);
9762
+ return next++;
9763
+ };
9764
+ for (const id of canonicalIds) toLegacy(id);
9765
+ return {
9766
+ toLegacy,
9767
+ toCanonical: (id) => typeof id === "number" ? legacyToCanonical.get(id) ?? null : canonicalToLegacy.has(String(id)) ? String(id) : null
9768
+ };
9769
+ }
9770
+ function toLegacyTasks(state, ids) {
9516
9771
  const levelById = computeLevels(state);
9517
9772
  const leveled = /* @__PURE__ */ new Map();
9518
9773
  state.forEachActivity((activity, id) => {
9519
9774
  const key = String(id);
9520
9775
  leveled.set(key, {
9521
- id: Number(id),
9522
- parent: activity.parentId === null ? 0 : Number(activity.parentId),
9776
+ id: ids.toLegacy(key),
9777
+ parent: activity.parentId === null ? 0 : ids.toLegacy(activity.parentId),
9523
9778
  $level: levelById.get(key) ?? 0,
9524
9779
  $source: state.getOutgoingLinkIds(key),
9525
9780
  $target: state.getIncomingLinkIds(key),
@@ -9593,7 +9848,8 @@ async function createGanttAdapter(state, hoursPerDay) {
9593
9848
  };
9594
9849
  const bounds = projectBounds(state);
9595
9850
  await yieldToBrowser();
9596
- const leveled = toLegacyTasks(state);
9851
+ const ids = createLegacyIds(state);
9852
+ const leveled = toLegacyTasks(state, ids);
9597
9853
  await yieldToBrowser();
9598
9854
  const byTime = [...leveled.values()].sort(
9599
9855
  (a, b) => (a.start_date?.getTime() ?? 0) - (b.start_date?.getTime() ?? 0)
@@ -9603,24 +9859,31 @@ async function createGanttAdapter(state, hoursPerDay) {
9603
9859
  return {
9604
9860
  fullyParsed: true,
9605
9861
  config: { hoursPerDay },
9606
- getTask: (id) => leveled.get(String(id)) ?? null,
9862
+ getTask: (id) => {
9863
+ const canonicalId = ids.toCanonical(id);
9864
+ return canonicalId === null ? null : leveled.get(canonicalId) ?? null;
9865
+ },
9607
9866
  getLink: (id) => {
9608
9867
  const link = state.getLink(id);
9609
9868
  if (!link) return null;
9610
9869
  return {
9611
9870
  ...link,
9612
- source: Number(link.source),
9613
- target: Number(link.target),
9871
+ source: ids.toLegacy(link.source),
9872
+ target: ids.toLegacy(link.target),
9614
9873
  lag: link.lag
9615
9874
  };
9616
9875
  },
9617
9876
  getCalendar: resolveCalendar,
9618
- getChildren: (parentId) => state.getChildren(parentId).map((childId) => Number(childId)),
9877
+ getChildren: (parentId) => {
9878
+ const canonicalId = ids.toCanonical(parentId);
9879
+ return canonicalId === null ? [] : state.getChildren(canonicalId).map(ids.toLegacy);
9880
+ },
9619
9881
  eachTask: (cb) => {
9620
9882
  for (const task of treeOrdered) cb(task);
9621
9883
  },
9622
9884
  getTaskByTime: () => byTime,
9623
9885
  getSubtaskDates: () => bounds,
9886
+ getCanonicalActivityId: ids.toCanonical,
9624
9887
  refreshData: () => {
9625
9888
  }
9626
9889
  };
@@ -9628,21 +9891,92 @@ async function createGanttAdapter(state, hoursPerDay) {
9628
9891
 
9629
9892
  // src/critical-path/run-critical-path.ts
9630
9893
  var CP_WRITE_YIELD_EVERY = 1e3;
9631
- function fieldsForActivity(activityId, progress, maps) {
9632
- const toDate = (value) => value == null ? null : new Date(value);
9894
+ function fieldsForLink(rawResidual, sourceIsCritical, targetIsCritical, targetProgress) {
9895
+ const freeSlackHours = typeof rawResidual === "number" && Number.isFinite(rawResidual) ? Object.is(rawResidual, -0) ? 0 : rawResidual : null;
9896
+ return {
9897
+ freeSlackHours,
9898
+ isCritical: freeSlackHours !== null && freeSlackHours <= 0 && sourceIsCritical && targetIsCritical && targetProgress === 0
9899
+ };
9900
+ }
9901
+ async function collectActivityFields(state, maps, hoursPerDay, toCanonicalActivityId, isCurrent) {
9902
+ const fields = /* @__PURE__ */ new Map();
9903
+ let sinceYield = 0;
9904
+ for (const calculationId of maps.forward?.keys() ?? []) {
9905
+ if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9906
+ sinceYield = 0;
9907
+ await yieldToBrowser();
9908
+ if (!isCurrent()) return null;
9909
+ }
9910
+ const activityId = toCanonicalActivityId(calculationId);
9911
+ if (activityId === null) continue;
9912
+ const activity = state.getActivity(activityId);
9913
+ if (!activity) continue;
9914
+ fields.set(
9915
+ String(activityId),
9916
+ fieldsForActivity(
9917
+ calculationId,
9918
+ Number(activity.progress ?? 0),
9919
+ maps,
9920
+ hoursPerDay
9921
+ )
9922
+ );
9923
+ }
9924
+ return isCurrent() ? fields : null;
9925
+ }
9926
+ async function collectLinkFields(state, linkResidualHours, activityFields, isCurrent) {
9927
+ const fields = /* @__PURE__ */ new Map();
9928
+ let sinceYield = 0;
9929
+ for (const link of state.getAllLinks()) {
9930
+ if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9931
+ sinceYield = 0;
9932
+ await yieldToBrowser();
9933
+ if (!isCurrent()) return null;
9934
+ }
9935
+ fields.set(
9936
+ String(link.id),
9937
+ fieldsForLink(
9938
+ linkResidualHours.get(String(link.id)),
9939
+ activityFields.get(link.source)?.isCritical === true,
9940
+ activityFields.get(link.target)?.isCritical === true,
9941
+ state.getActivity(link.target)?.progress
9942
+ )
9943
+ );
9944
+ }
9945
+ return isCurrent() ? fields : null;
9946
+ }
9947
+ var slackDaysToHours = (valueDays, hoursPerDay) => {
9948
+ if (typeof valueDays !== "number" || !Number.isFinite(valueDays) || !Number.isFinite(hoursPerDay) || hoursPerDay <= 0) {
9949
+ return null;
9950
+ }
9951
+ const hours = valueDays * hoursPerDay;
9952
+ if (!Number.isFinite(hours)) return null;
9953
+ return Object.is(hours, -0) ? 0 : hours;
9954
+ };
9955
+ function fieldsForActivity(activityId, progress, maps, hoursPerDay) {
9956
+ const toDate = (value) => {
9957
+ if (value == null) return null;
9958
+ const date2 = new Date(value);
9959
+ return Number.isFinite(date2.getTime()) ? date2 : null;
9960
+ };
9633
9961
  const forward = maps.forward?.get(activityId);
9634
9962
  const backward = maps.backward?.get(activityId);
9635
- const totalSlack = maps.totalFloat.get(activityId) ?? null;
9963
+ const totalSlackHours = slackDaysToHours(
9964
+ maps.totalFloat.get(activityId),
9965
+ hoursPerDay
9966
+ );
9636
9967
  return {
9637
9968
  criticalPath: {
9638
9969
  earlyStart: toDate(forward?.es),
9639
9970
  earlyFinish: toDate(forward?.ef),
9640
9971
  lateStart: toDate(backward?.ls),
9641
9972
  lateFinish: toDate(backward?.lf),
9642
- totalSlackHours: totalSlack
9973
+ totalSlackHours
9643
9974
  },
9644
- isCritical: progress < 100 && totalSlack != null && totalSlack <= 0,
9645
- freeSlackHours: maps.freeFloat.get(activityId)?.activityFreeFloat ?? null
9975
+ isCritical: progress < 100 && totalSlackHours !== null && totalSlackHours <= 0,
9976
+ freeSlackHours: slackDaysToHours(
9977
+ maps.freeFloat.get(activityId)?.activityFreeFloat,
9978
+ hoursPerDay
9979
+ )
9646
9980
  };
9647
9981
  }
9648
9982
  var datesEqual = (left, right) => left === right || left !== null && right !== null && left.getTime() === right.getTime();
@@ -9655,6 +9989,14 @@ function setCriticalPathFieldsIfChanged(state, activityId, fields) {
9655
9989
  }
9656
9990
  state.setActivityFields(activityId, fields);
9657
9991
  }
9992
+ function setCriticalPathLinkFieldsIfChanged(state, linkId, fields) {
9993
+ const link = state.getLink(linkId);
9994
+ if (!link || Object.is(link.freeSlackHours, fields.freeSlackHours) && Object.is(link.isCritical, fields.isCritical)) {
9995
+ return;
9996
+ }
9997
+ state.setLinkField(linkId, "freeSlackHours", fields.freeSlackHours);
9998
+ state.setLinkField(linkId, "isCritical", fields.isCritical);
9999
+ }
9658
10000
  async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => true) {
9659
10001
  if (!isCurrent()) return null;
9660
10002
  const gantt = await createGanttAdapter(state, hoursPerDay);
@@ -9672,37 +10014,34 @@ async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => t
9672
10014
  totalFloat: criticalPath.totalFloat,
9673
10015
  freeFloat: criticalPath.freeFloat
9674
10016
  };
9675
- const activityIds = /* @__PURE__ */ new Set([...maps.forward?.keys() ?? []]);
9676
- const fieldsByActivity = /* @__PURE__ */ new Map();
9677
- let sinceYield = 0;
9678
- for (const activityId of activityIds) {
9679
- if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9680
- sinceYield = 0;
9681
- await yieldToBrowser();
9682
- if (!isCurrent()) return null;
9683
- }
9684
- const activity = state.getActivity(activityId);
9685
- if (!activity) continue;
9686
- const progress = Number(activity.progress ?? 0);
9687
- fieldsByActivity.set(
9688
- activityId,
9689
- fieldsForActivity(activityId, progress, maps)
9690
- );
9691
- }
9692
- return isCurrent() ? fieldsByActivity : null;
9693
- }
9694
- async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9695
- const fieldsByActivity = await computeCriticalPathFields(
10017
+ const activities = await collectActivityFields(
9696
10018
  state,
10019
+ maps,
9697
10020
  hoursPerDay,
10021
+ gantt.getCanonicalActivityId,
10022
+ isCurrent
10023
+ );
10024
+ if (!activities) return null;
10025
+ const links = await collectLinkFields(
10026
+ state,
10027
+ criticalPath.linkResidualHours,
10028
+ activities,
9698
10029
  isCurrent
9699
10030
  );
9700
- if (!fieldsByActivity || !isCurrent()) return false;
9701
- for (const [activityId, fields] of fieldsByActivity) {
10031
+ return links ? { activities, links } : null;
10032
+ }
10033
+ async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
10034
+ const fields = await computeCriticalPathFields(state, hoursPerDay, isCurrent);
10035
+ if (!fields || !isCurrent()) return false;
10036
+ for (const [activityId, activityFields] of fields.activities) {
9702
10037
  if (!isCurrent()) return false;
9703
- setCriticalPathFieldsIfChanged(state, activityId, fields);
10038
+ setCriticalPathFieldsIfChanged(state, activityId, activityFields);
9704
10039
  }
9705
- return true;
10040
+ for (const [linkId, linkFields] of fields.links) {
10041
+ if (!isCurrent()) return false;
10042
+ setCriticalPathLinkFieldsIfChanged(state, linkId, linkFields);
10043
+ }
10044
+ return true;
9706
10045
  }
9707
10046
 
9708
10047
  // src/propagations/upward/real-cost.ts
@@ -9802,9 +10141,15 @@ function isSchedulingDispatch(action, structuralStaleKinds) {
9802
10141
  return true;
9803
10142
  }
9804
10143
  function dispatchChangesSchedulingState(action) {
10144
+ if (action.kind === "activity-lookahead-sync") {
10145
+ return action.persistedProgress !== void 0;
10146
+ }
9805
10147
  return isSchedulingDispatch(action, NO_STRUCTURAL_EXEMPTIONS);
9806
10148
  }
9807
10149
  function resolveRunCriticalPath(action) {
10150
+ if (action.kind === "activity-lookahead-sync") {
10151
+ return action.persistedProgress !== void 0;
10152
+ }
9808
10153
  return isSchedulingDispatch(
9809
10154
  action,
9810
10155
  STRUCTURAL_KINDS_LEAVING_CRITICAL_PATH_STALE
@@ -10090,6 +10435,23 @@ function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
10090
10435
  return effects;
10091
10436
  }
10092
10437
 
10438
+ // src/dispatch/conversion.ts
10439
+ function snapshotToLink(l) {
10440
+ const view = {
10441
+ id: String(l.id),
10442
+ source: String(l.source),
10443
+ target: String(l.target),
10444
+ type: l.type,
10445
+ lag: Number(l.lag),
10446
+ freeSlackHours: typeof l.freeSlackHours === "number" && Number.isFinite(l.freeSlackHours) ? l.freeSlackHours : null,
10447
+ isCritical: l.isCritical === true
10448
+ };
10449
+ if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
10450
+ if (l.ganttId != null) view.ganttId = Number(l.ganttId);
10451
+ if (l.sectorId != null) view.sectorId = Number(l.sectorId);
10452
+ return view;
10453
+ }
10454
+
10093
10455
  // src/dispatch/shared/change-set.ts
10094
10456
  async function assembleChangeSet(adapter, args) {
10095
10457
  const allTouched = collectChangeSetIds(adapter, args);
@@ -10099,7 +10461,7 @@ async function assembleChangeSet(adapter, args) {
10099
10461
  const insertedIds = collectInsertedIds(captured);
10100
10462
  mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
10101
10463
  }
10102
- const sirDetection = args.preview === true ? void 0 : args.sirDetection;
10464
+ const sirDetection = args.sirDetection;
10103
10465
  const beforeDiffEffects = sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
10104
10466
  const correlativeBefore = buildCorrelativeBeforeMap(
10105
10467
  args.correlativeShifts,
@@ -10113,16 +10475,89 @@ async function assembleChangeSet(adapter, args) {
10113
10475
  );
10114
10476
  const effects = sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
10115
10477
  const warnings = args.warnings ?? [];
10478
+ const linkChanges = mergeLinkChanges(
10479
+ args.links ?? [],
10480
+ captured ? buildCapturedCriticalPathLinkChanges(adapter, captured) : []
10481
+ );
10116
10482
  return {
10117
10483
  source: args.source,
10118
10484
  activities: activityChanges,
10119
- links: args.links ?? [],
10485
+ links: linkChanges,
10120
10486
  calendars: [],
10121
10487
  trackingEvents: args.trackingEvents ?? [],
10122
10488
  ...effects.length > 0 ? { effects } : {},
10123
10489
  ...warnings.length > 0 ? { warnings } : {}
10124
10490
  };
10125
10491
  }
10492
+ function buildCapturedCriticalPathLinkChanges(adapter, captured) {
10493
+ const changes = [];
10494
+ for (const operation of captured.ops) {
10495
+ if (operation.kind !== "link-field") continue;
10496
+ const after = adapter.getLink(operation.linkId);
10497
+ if (!after) continue;
10498
+ const fields = {};
10499
+ for (const field of ["freeSlackHours", "isCritical"]) {
10500
+ if (!Object.is(operation.before[field], after[field])) {
10501
+ fields[field] = {
10502
+ before: operation.before[field],
10503
+ after: after[field]
10504
+ };
10505
+ }
10506
+ }
10507
+ if (Object.keys(fields).length === 0) continue;
10508
+ changes.push({
10509
+ id: operation.linkId,
10510
+ kind: "updated",
10511
+ fields,
10512
+ after: snapshotToLink(after)
10513
+ });
10514
+ }
10515
+ return changes;
10516
+ }
10517
+ function mergeLinkChanges(explicit, captured) {
10518
+ const merged = [];
10519
+ const indexById = /* @__PURE__ */ new Map();
10520
+ for (const change of explicit) {
10521
+ const index = indexById.get(change.id);
10522
+ if (index === void 0) {
10523
+ indexById.set(change.id, merged.length);
10524
+ merged.push(change);
10525
+ continue;
10526
+ }
10527
+ const current = merged[index];
10528
+ if ((current.kind === "updated" || current.kind === "created") && change.kind === "updated") {
10529
+ merged[index] = {
10530
+ ...current,
10531
+ fields: { ...current.fields, ...change.fields },
10532
+ after: change.after
10533
+ };
10534
+ continue;
10535
+ }
10536
+ merged[index] = change;
10537
+ }
10538
+ for (const change of captured) {
10539
+ const index = indexById.get(change.id);
10540
+ if (index === void 0) {
10541
+ indexById.set(change.id, merged.length);
10542
+ merged.push(change);
10543
+ continue;
10544
+ }
10545
+ const current = merged[index];
10546
+ if (current.kind === "deleted") continue;
10547
+ const capturedFields = current.kind === "created" ? Object.fromEntries(
10548
+ Object.entries(change.fields ?? {}).map(([field, value]) => [
10549
+ field,
10550
+ { before: void 0, after: value.after }
10551
+ ])
10552
+ ) : change.fields;
10553
+ merged[index] = {
10554
+ ...current,
10555
+ fields: { ...current.fields, ...capturedFields },
10556
+ after: change.after
10557
+ };
10558
+ }
10559
+ return merged;
10560
+ }
10126
10561
  function buildCorrelativeBeforeMap(shifts, touched) {
10127
10562
  if (!shifts || shifts.length === 0) return void 0;
10128
10563
  const correlativeBefore = /* @__PURE__ */ new Map();
@@ -10158,21 +10593,6 @@ function collectInsertedIds(captured) {
10158
10593
  );
10159
10594
  }
10160
10595
 
10161
- // src/dispatch/conversion.ts
10162
- function snapshotToLink(l) {
10163
- const view = {
10164
- id: String(l.id),
10165
- source: String(l.source),
10166
- target: String(l.target),
10167
- type: l.type,
10168
- lag: Number(l.lag)
10169
- };
10170
- if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
10171
- if (l.ganttId != null) view.ganttId = Number(l.ganttId);
10172
- if (l.sectorId != null) view.sectorId = Number(l.sectorId);
10173
- return view;
10174
- }
10175
-
10176
10596
  // src/dispatch/shared/helpers.ts
10177
10597
  function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
10178
10598
  const out = [];
@@ -10440,7 +10860,6 @@ async function dispatchInlineEdit(action, options, deps) {
10440
10860
  touchedIds,
10441
10861
  scheduledIds,
10442
10862
  sirDetection: "before-diff",
10443
- preview: options.preview,
10444
10863
  links: linkChanges,
10445
10864
  trackingEvents,
10446
10865
  ...constraintWarning ? { warnings: [constraintWarning] } : {},
@@ -10518,6 +10937,26 @@ function collectDeterministicIds(operations) {
10518
10937
  });
10519
10938
  return ids.size > 0 ? ids : void 0;
10520
10939
  }
10940
+ function resolveDeterministicIds(deterministicIds, deps) {
10941
+ if (deterministicIds === void 0) return void 0;
10942
+ const resolved = /* @__PURE__ */ new Map();
10943
+ const assigned = /* @__PURE__ */ new Set();
10944
+ const nextAvailableId = () => {
10945
+ let candidate = deps.linkIdGen.next();
10946
+ while (deps.adapter.getLink(candidate) !== null || deps.isLinkIdReserved(String(candidate)) || assigned.has(String(candidate))) {
10947
+ candidate = deps.linkIdGen.next();
10948
+ }
10949
+ return candidate;
10950
+ };
10951
+ for (const [operationIndex, explicitId] of deterministicIds) {
10952
+ const isReserved = deps.adapter.getLink(explicitId) !== null || deps.isLinkIdReserved(String(explicitId)) || assigned.has(String(explicitId));
10953
+ const linkId = isReserved ? nextAvailableId() : explicitId;
10954
+ if (!isReserved) deps.linkIdGen.bump(explicitId);
10955
+ assigned.add(String(linkId));
10956
+ resolved.set(operationIndex, linkId);
10957
+ }
10958
+ return resolved;
10959
+ }
10521
10960
  function batchOpToLinkOperation(op, hoursPerDay) {
10522
10961
  if (op.kind === "create") {
10523
10962
  return {
@@ -10550,7 +10989,7 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
10550
10989
  operations,
10551
10990
  adapter,
10552
10991
  linkIdGen,
10553
- deterministicIds
10992
+ resolveDeterministicIds(deterministicIds, deps)
10554
10993
  );
10555
10994
  if (operations.length === 1 && applied[0] && applied[0].rejected !== null && source.kind !== "inline-edit") {
10556
10995
  return { ok: false, reason: applied[0].rejected };
@@ -10732,26 +11171,19 @@ function recomputeHhCascadeForParent(startParentId, adapter) {
10732
11171
  if (!startParentId || startParentId === "0") return;
10733
11172
  let currentParentId = String(startParentId);
10734
11173
  while (currentParentId !== null) {
10735
- const childIds = adapter.getChildren(currentParentId);
10736
- let hhSum = 0;
10737
- let rwSum = 0;
10738
- for (const childId of childIds) {
10739
- const child = adapter.getActivity(childId);
10740
- if (!child) continue;
10741
- const hh = readNumber(child, ACTIVITY_PROPERTY.WORK_HOURS);
10742
- const rw = readChildRealWork(childId, child, hh, adapter);
10743
- hhSum += hh;
10744
- rwSum += rw;
10745
- }
11174
+ const { workHours, realWorkHours } = computeParentHhRollup(
11175
+ currentParentId,
11176
+ adapter
11177
+ );
10746
11178
  adapter.setActivityField(
10747
11179
  currentParentId,
10748
11180
  ACTIVITY_PROPERTY.WORK_HOURS,
10749
- hhSum
11181
+ workHours
10750
11182
  );
10751
11183
  adapter.setActivityField(
10752
11184
  currentParentId,
10753
11185
  ACTIVITY_PROPERTY.REAL_WORK_HOURS,
10754
- rwSum
11186
+ realWorkHours
10755
11187
  );
10756
11188
  const parentSnap = adapter.getActivity(currentParentId);
10757
11189
  if (!parentSnap) break;
@@ -10767,6 +11199,40 @@ function recomputeHhCascadeForActivity(activityId, adapter) {
10767
11199
  if (!parentId || isRootParent(parentId)) return;
10768
11200
  recomputeHhCascadeForParent(parentId, adapter);
10769
11201
  }
11202
+ function recomputeRealWorkFromProgress(progressIds, parentIdsDeepestFirst, adapter) {
11203
+ for (const activityId of progressIds) {
11204
+ if (adapter.hasChildren(activityId)) continue;
11205
+ const activity = adapter.getActivity(activityId);
11206
+ if (!activity) continue;
11207
+ adapter.setActivityField(
11208
+ activityId,
11209
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
11210
+ computeLeafRealWork(
11211
+ readNumber(activity, ACTIVITY_PROPERTY.WORK_HOURS),
11212
+ readNumber(activity, ACTIVITY_PROPERTY.PROGRESS)
11213
+ )
11214
+ );
11215
+ }
11216
+ for (const parentId of parentIdsDeepestFirst) {
11217
+ adapter.setActivityField(
11218
+ parentId,
11219
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
11220
+ computeParentHhRollup(parentId, adapter).realWorkHours
11221
+ );
11222
+ }
11223
+ }
11224
+ function computeParentHhRollup(parentId, adapter) {
11225
+ let workHours = 0;
11226
+ let realWorkHours = 0;
11227
+ for (const childId of adapter.getChildren(parentId)) {
11228
+ const child = adapter.getActivity(childId);
11229
+ if (!child) continue;
11230
+ const childWorkHours = readNumber(child, ACTIVITY_PROPERTY.WORK_HOURS);
11231
+ workHours += childWorkHours;
11232
+ realWorkHours += readChildRealWork(childId, child, childWorkHours, adapter);
11233
+ }
11234
+ return { workHours, realWorkHours };
11235
+ }
10770
11236
  function readNumber(activity, key) {
10771
11237
  const v = activity[key];
10772
11238
  if (typeof v === "number" && Number.isFinite(v)) return v;
@@ -11139,6 +11605,36 @@ function computeFractionalCidAtIndex(adapter, parentKey, targetIndex, excludeId)
11139
11605
  return (before + after) / 2;
11140
11606
  }
11141
11607
 
11608
+ // src/dispatch/creation-overrides.ts
11609
+ var PUBLIC_CREATION_OVERRIDE_FIELDS = /* @__PURE__ */ new Set([
11610
+ ...[...CREATION_OVERRIDE_FIELDS].filter((field) => field !== "durationHours"),
11611
+ "durationDays"
11612
+ ]);
11613
+ function normalizeActivityCreationOverrides(raw, hoursPerDay) {
11614
+ if (raw === void 0) return { ok: true, value: {} };
11615
+ const prototype = Object.getPrototypeOf(raw);
11616
+ if (prototype !== null && prototype !== Object.prototype) {
11617
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11618
+ }
11619
+ for (const key of Reflect.ownKeys(raw)) {
11620
+ if (typeof key === "symbol" || !PUBLIC_CREATION_OVERRIDE_FIELDS.has(key)) {
11621
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11622
+ }
11623
+ }
11624
+ const { durationDays, ...rest } = raw;
11625
+ if (durationDays !== void 0 && (!Number.isFinite(durationDays) || durationDays < 0)) {
11626
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11627
+ }
11628
+ return {
11629
+ ok: true,
11630
+ value: {
11631
+ ...rest,
11632
+ ...durationDays === 0 && (rest.type === void 0 || rest.type === "task") ? { type: "milestone" } : {},
11633
+ ...durationDays === void 0 ? {} : { durationHours: durationToHours(durationDays, hoursPerDay) }
11634
+ }
11635
+ };
11636
+ }
11637
+
11142
11638
  // src/dispatch/handlers/activity-create.ts
11143
11639
  async function createActivityCore(action, deps, opts = {}) {
11144
11640
  const { adapter, scheduler, sector, calendars, activityIdGen, uidGen } = deps;
@@ -11146,7 +11642,12 @@ async function createActivityCore(action, deps, opts = {}) {
11146
11642
  const target = validateCreateTarget(action, adapter);
11147
11643
  if (!target.ok) return target;
11148
11644
  const { parent, anchorSiblingId, isRoot } = target;
11149
- const newId = resolveNewActivityId(action.activityId, adapter, activityIdGen);
11645
+ const newId = resolveNewActivityId(
11646
+ action.activityId,
11647
+ adapter,
11648
+ activityIdGen,
11649
+ deps.isActivityIdReserved
11650
+ );
11150
11651
  const newUid = uidGen.next();
11151
11652
  const activityInput = {
11152
11653
  id: newId,
@@ -11239,9 +11740,18 @@ function validateAnchorSibling(action, adapter, isRoot, anchorSiblingId) {
11239
11740
  }
11240
11741
  return null;
11241
11742
  }
11242
- function resolveNewActivityId(explicitId, adapter, activityIdGen) {
11243
- if (explicitId === void 0) return activityIdGen.next();
11244
- if (adapter.activityExists(explicitId)) return activityIdGen.next();
11743
+ function resolveNewActivityId(explicitId, adapter, activityIdGen, isActivityIdReserved) {
11744
+ const nextAvailableId = () => {
11745
+ let candidate = activityIdGen.next();
11746
+ while (adapter.activityExists(candidate) || isActivityIdReserved(String(candidate))) {
11747
+ candidate = activityIdGen.next();
11748
+ }
11749
+ return candidate;
11750
+ };
11751
+ if (explicitId === void 0) return nextAvailableId();
11752
+ if (adapter.activityExists(explicitId) || isActivityIdReserved(String(explicitId))) {
11753
+ return nextAvailableId();
11754
+ }
11245
11755
  activityIdGen.bump(explicitId);
11246
11756
  return explicitId;
11247
11757
  }
@@ -11320,7 +11830,15 @@ async function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, h
11320
11830
  }
11321
11831
  async function dispatchActivityCreate(action, options, deps) {
11322
11832
  const { adapter, scheduler, sector } = deps;
11323
- const core = await createActivityCore(action, deps);
11833
+ const normalized2 = normalizeActivityCreationOverrides(
11834
+ action.overrides,
11835
+ sector.hoursPerDay
11836
+ );
11837
+ if (!normalized2.ok) return normalized2;
11838
+ const core = await createActivityCore(
11839
+ { ...action, overrides: normalized2.value },
11840
+ deps
11841
+ );
11324
11842
  if (!core.ok) return core;
11325
11843
  const { newId } = core;
11326
11844
  const createdAutoScheduling = core.newActivity.autoScheduling;
@@ -11532,7 +12050,7 @@ async function dispatchActivityDelete(action, options, deps) {
11532
12050
  for (const linkId of incidentLinkIds) {
11533
12051
  adapter.removeLink(linkId);
11534
12052
  }
11535
- const viewStateChanges = captureDeletedViewState(adapter, toDelete);
12053
+ const viewStateChanges2 = captureDeletedViewState(adapter, toDelete);
11536
12054
  let removed = 0;
11537
12055
  for (const id of toDelete) {
11538
12056
  removed += 1;
@@ -11614,7 +12132,6 @@ async function dispatchActivityDelete(action, options, deps) {
11614
12132
  touchedIds,
11615
12133
  scheduledIds,
11616
12134
  sirDetection: "after-diff",
11617
- preview: options.preview,
11618
12135
  links: buildLinkDeletions(beforeLinks),
11619
12136
  trackingEvents: [trackingEvent],
11620
12137
  hoursPerDay: sector.hoursPerDay,
@@ -11622,7 +12139,7 @@ async function dispatchActivityDelete(action, options, deps) {
11622
12139
  });
11623
12140
  return {
11624
12141
  ok: true,
11625
- changes: viewStateChanges.length > 0 ? { ...changes, viewState: viewStateChanges } : changes,
12142
+ changes: viewStateChanges2.length > 0 ? { ...changes, viewState: viewStateChanges2 } : changes,
11626
12143
  __beforeSnap: deletedRowsSnap,
11627
12144
  __beforeLinks: beforeLinksByStringId
11628
12145
  };
@@ -11730,7 +12247,7 @@ async function dispatchActivityBatch(action, options, deps) {
11730
12247
  source: link.source,
11731
12248
  target: link.target,
11732
12249
  type: link.type,
11733
- lag: options.inputUnit === "hours" ? link.lag : lagDaysToHours(link.lag, deps.sector.hoursPerDay)
12250
+ lag: lagDaysToHours(link.lag, deps.sector.hoursPerDay)
11734
12251
  },
11735
12252
  linkId: link.linkId
11736
12253
  }));
@@ -11775,7 +12292,6 @@ async function dispatchActivityBatch(action, options, deps) {
11775
12292
  links: buildLinkChangesForBatch(appliedLinks, beforeLinks, deps.adapter),
11776
12293
  hoursPerDay: deps.sector.hoursPerDay,
11777
12294
  correlativeShifts: existingCorrelativeShifts,
11778
- preview: options.preview,
11779
12295
  ...action.mode === "replace" ? { sirDetection: "after-diff" } : {}
11780
12296
  });
11781
12297
  return {
@@ -11803,7 +12319,7 @@ function validateBatch(action, deps) {
11803
12319
  }
11804
12320
  if (item.activityId !== void 0) {
11805
12321
  const activityId = String(item.activityId);
11806
- if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId) || action.mode === "append" && deps.adapter.getActivity(activityId) !== null) {
12322
+ if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId)) {
11807
12323
  return REJECTION_REASON.INVALID;
11808
12324
  }
11809
12325
  explicitActivityIds.add(activityId);
@@ -11825,7 +12341,7 @@ function validateBatch(action, deps) {
11825
12341
  }
11826
12342
  if (link.linkId !== void 0) {
11827
12343
  const linkId = String(link.linkId);
11828
- if (linkId.length === 0 || explicitLinkIds.has(linkId) || action.mode === "append" && deps.adapter.getLink(linkId) !== null) {
12344
+ if (linkId.length === 0 || explicitLinkIds.has(linkId)) {
11829
12345
  return REJECTION_REASON.INVALID;
11830
12346
  }
11831
12347
  explicitLinkIds.add(linkId);
@@ -11861,24 +12377,38 @@ function hasParentCycle(items) {
11861
12377
  return false;
11862
12378
  }
11863
12379
  function buildBatchPlan(action, deps) {
12380
+ const normalizedOverrides = /* @__PURE__ */ new Map();
11864
12381
  for (const item of action.items) {
11865
- if (item.activityId !== void 0) {
11866
- deps.activityIdGen.bump(item.activityId);
11867
- }
12382
+ const normalized2 = normalizeActivityCreationOverrides(
12383
+ item.overrides,
12384
+ deps.sector.hoursPerDay
12385
+ );
12386
+ if (!normalized2.ok) return normalized2;
12387
+ normalizedOverrides.set(item.batchId, normalized2.value);
11868
12388
  }
11869
12389
  const activityIdsByBatchId = /* @__PURE__ */ new Map();
12390
+ const plannedActivityIds = /* @__PURE__ */ new Set();
11870
12391
  for (const item of action.items) {
11871
- activityIdsByBatchId.set(
11872
- item.batchId,
11873
- item.activityId ?? deps.activityIdGen.next()
12392
+ const activityId = resolvePlannedActivityId(
12393
+ item.activityId,
12394
+ action.mode,
12395
+ plannedActivityIds,
12396
+ deps
11874
12397
  );
11875
- }
11876
- for (const link of action.links) {
11877
- if (link.linkId !== void 0) deps.linkIdGen.bump(link.linkId);
11878
- }
11879
- const linkIds = action.links.map(
11880
- (link) => link.linkId ?? deps.linkIdGen.next()
11881
- );
12398
+ plannedActivityIds.add(String(activityId));
12399
+ activityIdsByBatchId.set(item.batchId, activityId);
12400
+ }
12401
+ const plannedLinkIds = /* @__PURE__ */ new Set();
12402
+ const linkIds = action.links.map((link) => {
12403
+ const linkId = resolvePlannedLinkId(
12404
+ link.linkId,
12405
+ action.mode,
12406
+ plannedLinkIds,
12407
+ deps
12408
+ );
12409
+ plannedLinkIds.add(String(linkId));
12410
+ return linkId;
12411
+ });
11882
12412
  const insertionCorrelativeIds = planInsertionCorrelatives(
11883
12413
  action,
11884
12414
  activityIdsByBatchId,
@@ -11901,7 +12431,8 @@ function buildBatchPlan(action, deps) {
11901
12431
  activityId,
11902
12432
  parentId,
11903
12433
  parentBatchId,
11904
- insertionCorrelativeId
12434
+ insertionCorrelativeId,
12435
+ overrides: normalizedOverrides.get(item.batchId) ?? {}
11905
12436
  });
11906
12437
  }
11907
12438
  const links = [];
@@ -11922,6 +12453,30 @@ function buildBatchPlan(action, deps) {
11922
12453
  }
11923
12454
  return { ok: true, plan: { items, links } };
11924
12455
  }
12456
+ function resolvePlannedActivityId(explicitId, mode, plannedIds, deps) {
12457
+ const explicitIsAvailable = explicitId !== void 0 && (mode === "replace" || deps.adapter.getActivity(explicitId) === null && !deps.isActivityIdReserved(String(explicitId))) && !plannedIds.has(String(explicitId));
12458
+ if (explicitIsAvailable) {
12459
+ deps.activityIdGen.bump(explicitId);
12460
+ return explicitId;
12461
+ }
12462
+ let candidate = deps.activityIdGen.next();
12463
+ while (plannedIds.has(String(candidate)) || mode === "append" && (deps.adapter.getActivity(candidate) !== null || deps.isActivityIdReserved(String(candidate)))) {
12464
+ candidate = deps.activityIdGen.next();
12465
+ }
12466
+ return candidate;
12467
+ }
12468
+ function resolvePlannedLinkId(explicitId, mode, plannedIds, deps) {
12469
+ const explicitIsAvailable = explicitId !== void 0 && (mode === "replace" || deps.adapter.getLink(explicitId) === null && !deps.isLinkIdReserved(String(explicitId))) && !plannedIds.has(String(explicitId));
12470
+ if (explicitIsAvailable) {
12471
+ deps.linkIdGen.bump(explicitId);
12472
+ return explicitId;
12473
+ }
12474
+ let candidate = deps.linkIdGen.next();
12475
+ while (plannedIds.has(String(candidate)) || mode === "append" && (deps.adapter.getLink(candidate) !== null || deps.isLinkIdReserved(String(candidate)))) {
12476
+ candidate = deps.linkIdGen.next();
12477
+ }
12478
+ return candidate;
12479
+ }
11925
12480
  function planInsertionCorrelatives(action, activityIdsByBatchId, deps) {
11926
12481
  if (isSimpleRootAppend(action)) {
11927
12482
  const firstCorrelativeId = deps.adapter.activityCount();
@@ -12031,7 +12586,7 @@ async function createPlannedActivities(action, plannedItems, deps) {
12031
12586
  {
12032
12587
  parentId: item.parentId,
12033
12588
  activityId: item.activityId,
12034
- ...item.input.overrides === void 0 ? {} : { overrides: item.input.overrides },
12589
+ overrides: item.overrides,
12035
12590
  ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
12036
12591
  },
12037
12592
  deps,
@@ -12108,301 +12663,280 @@ function dispatchSirSync(action, deps) {
12108
12663
  };
12109
12664
  }
12110
12665
 
12111
- // src/dispatch/handlers/bulk-edit.ts
12112
- var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
12113
- async function dispatchBulkEdit(action, options, deps) {
12114
- const { adapter, scheduler, sector } = deps;
12115
- const editsIncludeDuration = action.edits.some(
12116
- (edit) => edit.column === COLUMN.DURATION
12666
+ // src/propagations/upward/progress-rollup.ts
12667
+ function recomputeAllProgressRollup(adapter) {
12668
+ for (const parentId of parentsDeepestFirst(adapter)) {
12669
+ rollupParent(parentId, adapter);
12670
+ }
12671
+ }
12672
+ function recomputeProgressRollupForParents(parentIdsDeepestFirst, adapter) {
12673
+ for (const parentId of parentIdsDeepestFirst) {
12674
+ rollupParent(parentId, adapter);
12675
+ }
12676
+ }
12677
+ function parentsDeepestFirst(adapter) {
12678
+ const parents = /* @__PURE__ */ new Set();
12679
+ adapter.forEachActivity((_activity, id) => {
12680
+ if (adapter.getChildren(id).length > 0) parents.add(id);
12681
+ });
12682
+ const depthById = /* @__PURE__ */ new Map();
12683
+ for (const id of parents) depthById.set(id, depthOf2(id, adapter));
12684
+ return [...parents].sort(
12685
+ (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
12117
12686
  );
12118
- if (editsIncludeDuration) {
12119
- scheduler.invalidateExpandedLinksCache();
12687
+ }
12688
+ function depthOf2(id, adapter) {
12689
+ let depth = 0;
12690
+ let current = id;
12691
+ while (current && current !== "0") {
12692
+ if (!adapter.getActivity(current)) break;
12693
+ depth += 1;
12694
+ current = adapter.getParentId(current);
12120
12695
  }
12121
- const verdicts = [];
12122
- const beforeSnap = /* @__PURE__ */ new Map();
12123
- const constraintPriorsByActivity = /* @__PURE__ */ new Map();
12124
- const explicitDateEditActivities = /* @__PURE__ */ new Set();
12125
- const touchedIds = /* @__PURE__ */ new Set();
12126
- const trackingEvents = [];
12127
- const linkChanges = [];
12128
- const warnings = [];
12129
- let needsAutoSchedule = false;
12130
- for (const edit of action.edits) {
12131
- const outcome = applyOneEdit(edit, deps);
12132
- verdicts.push(outcome.verdict);
12133
- if (!outcome.verdict.ok || !outcome.changes) continue;
12134
- const constraintWarning = toConstraintWarning(
12135
- edit.activityId,
12136
- outcome.changes.constraintWarning
12696
+ return depth;
12697
+ }
12698
+ function rollupParent(parentId, adapter) {
12699
+ const childIds = adapter.getChildren(parentId);
12700
+ if (childIds.length === 0) return;
12701
+ const children = [];
12702
+ for (const childId of childIds) {
12703
+ const child = adapter.getActivity(childId);
12704
+ if (!child) continue;
12705
+ children.push({
12706
+ progress: Number(child.progress ?? 0),
12707
+ ponderator: Number(child.ponderator ?? 0)
12708
+ });
12709
+ }
12710
+ const rollup = computeWeightedProgressRollup(children);
12711
+ if (rollup !== null) {
12712
+ adapter.setActivityField(
12713
+ parentId,
12714
+ COLUMN.PROGRESS,
12715
+ roundProgressPerLevel(rollup)
12137
12716
  );
12138
- if (constraintWarning) warnings.push(constraintWarning);
12139
- const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
12140
- mergeBeforeSnapshots(beforeSnap, editTouched, deps);
12141
- for (const touchedId of editTouched) touchedIds.add(touchedId);
12142
- recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
12143
- if (isExplicitConstraintDateEdit(edit.column)) {
12144
- explicitDateEditActivities.add(String(edit.activityId));
12717
+ }
12718
+ }
12719
+
12720
+ // src/dispatch/handlers/persistence-acknowledge.ts
12721
+ function persistedOwnersOf(entities) {
12722
+ const owners = /* @__PURE__ */ new Map();
12723
+ for (const entity of entities) {
12724
+ const proplannerId = entity.proplannerId;
12725
+ if (typeof proplannerId === "number" && Number.isSafeInteger(proplannerId) && proplannerId > 0) {
12726
+ owners.set(proplannerId, String(entity.id));
12145
12727
  }
12146
- const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
12147
- const preEditSnapshot = snapshotSingleActivity(
12148
- adapter,
12149
- String(edit.activityId)
12728
+ }
12729
+ return owners;
12730
+ }
12731
+ function assertAllNewEntitiesAcknowledged(entities, assignments, entityName) {
12732
+ const acknowledgedIds = new Set(assignments.map(({ id }) => String(id)));
12733
+ for (const entity of entities) {
12734
+ if ((entity.proplannerId == null || entity.proplannerId === 0) && !acknowledgedIds.has(String(entity.id))) {
12735
+ throw new Error(
12736
+ `persistence-acknowledge: missing ${entityName} proplannerId for "${String(entity.id)}"`
12737
+ );
12738
+ }
12739
+ }
12740
+ }
12741
+ function assertValidAssignment(assignment, entityName, readCurrent, ownerByBackendId, seenIds, seenBackendIds) {
12742
+ const id = String(assignment.id);
12743
+ if (id.length === 0 || seenIds.has(id)) {
12744
+ throw new Error(
12745
+ `persistence-acknowledge: duplicate or empty ${entityName} id "${id}"`
12150
12746
  );
12151
- const customIdBeforeApply = readLiveCustomId(edit, deps);
12152
- const changes = outcome.changes;
12153
- applyFieldChanges(adapter, edit.activityId, changes);
12154
- runPostProcessorsOnAdapter(
12155
- edit.activityId,
12156
- outcome.changes.postProcessors ?? [],
12157
- adapter,
12158
- preEditSnapshot
12747
+ }
12748
+ if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0) {
12749
+ throw new Error(
12750
+ `persistence-acknowledge: invalid ${entityName} proplannerId ${String(assignment.proplannerId)}`
12159
12751
  );
12160
- syncCustomIdTracker(edit, customIdBeforeApply, deps);
12161
- linkChanges.push(
12162
- ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
12752
+ }
12753
+ if (seenBackendIds.has(assignment.proplannerId)) {
12754
+ throw new Error(
12755
+ `persistence-acknowledge: duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
12163
12756
  );
12164
- needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
12165
- for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
12166
- trackingEvents.push({
12167
- name: trackingEvent.name,
12168
- properties: trackingEvent.properties ?? {}
12169
- });
12170
- }
12171
12757
  }
12172
- const postMutationArgs = {
12173
- action,
12174
- autoscheduleFrom: needsAutoSchedule ? "roots" : null,
12175
- recomputeParentsFrom: touchedIds,
12176
- now: deps.now,
12177
- options
12178
- };
12179
- const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
12180
- const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
12181
- {
12182
- adapter,
12183
- scheduler,
12184
- sector,
12185
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12186
- },
12187
- {
12188
- postMutationArgs,
12189
- revertTargets: [...constraintPriorsByActivity].filter(
12190
- ([activityId]) => !explicitDateEditActivities.has(activityId)
12191
- ).map(([activityId, priorConstraintDate]) => ({
12192
- activityId,
12193
- priorConstraintDate
12194
- })),
12195
- revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
12196
- activityId,
12197
- priorConstraintDate,
12198
- adapter,
12199
- deps.ctx.calendars
12200
- )
12201
- }
12202
- ) : runPostMutation(
12203
- {
12204
- adapter,
12205
- scheduler,
12206
- sector,
12207
- defaultBaseCalendarId: deps.defaultBaseCalendarId
12208
- },
12209
- postMutationArgs
12210
- ));
12211
- return {
12212
- ok: true,
12213
- verdicts,
12214
- changes: await assembleChangeSet(adapter, {
12215
- source: action,
12216
- beforeSnap,
12217
- touchedIds,
12218
- scheduledIds,
12219
- sirDetection: "before-diff",
12220
- preview: options.preview,
12221
- links: linkChanges,
12222
- trackingEvents,
12223
- warnings,
12224
- hoursPerDay: sector.hoursPerDay
12225
- })
12226
- };
12227
- }
12228
- function applyOneEdit(edit, deps) {
12229
- const { ctx } = deps;
12230
- const activityId = edit.activityId;
12231
- const pipeline = findPipelineForColumn(edit.column);
12232
- if (!pipeline) {
12233
- return {
12234
- verdict: {
12235
- activityId,
12236
- ok: false,
12237
- reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
12238
- },
12239
- changes: null
12240
- };
12241
- }
12242
- const activitySnap = ctx.activityReader.getActivity(edit.activityId);
12243
- if (!activitySnap) {
12244
- return {
12245
- verdict: {
12246
- activityId,
12247
- ok: false,
12248
- reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
12249
- },
12250
- changes: null
12251
- };
12252
- }
12253
- const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
12254
- if (!editGate.allowed) {
12255
- return {
12256
- verdict: {
12257
- activityId,
12258
- ok: false,
12259
- reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
12260
- ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
12261
- },
12262
- changes: null
12263
- };
12264
- }
12265
- const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
12266
- if (!parsed2.ok) {
12267
- return {
12268
- verdict: {
12269
- activityId,
12270
- ok: false,
12271
- reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
12272
- },
12273
- changes: null
12274
- };
12758
+ const state = readCurrent(id);
12759
+ if (!state.exists) {
12760
+ throw new Error(
12761
+ `persistence-acknowledge: unknown ${entityName} id "${id}"`
12762
+ );
12275
12763
  }
12276
- const oldValue = Reflect.get(activitySnap, pipeline.targetField);
12277
- const validation = pipeline.validate(
12278
- activitySnap,
12279
- oldValue,
12280
- parsed2.value,
12281
- ctx
12282
- );
12283
- if (!validation.valid) {
12284
- return {
12285
- verdict: {
12286
- activityId,
12287
- ok: false,
12288
- reason: validation.reason ?? REJECTION_REASON.INVALID,
12289
- ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
12290
- },
12291
- changes: null
12292
- };
12764
+ if (state.current !== null && state.current !== 0 && state.current !== assignment.proplannerId) {
12765
+ throw new Error(
12766
+ `persistence-acknowledge: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
12767
+ );
12293
12768
  }
12294
- if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
12295
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12296
- const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
12297
- const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
12298
- if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
12299
- return {
12300
- verdict: {
12301
- activityId,
12302
- ok: false,
12303
- reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
12304
- },
12305
- changes: null
12306
- };
12307
- }
12769
+ const existingOwner = ownerByBackendId.get(assignment.proplannerId);
12770
+ if (existingOwner !== void 0 && existingOwner !== id) {
12771
+ throw new Error(
12772
+ `persistence-acknowledge: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
12773
+ );
12308
12774
  }
12309
- const changes = pipeline.transform(
12310
- activitySnap,
12311
- parsed2.value,
12312
- ctx,
12313
- parsed2.raw
12314
- );
12315
- return { verdict: { activityId, ok: true }, changes };
12316
- }
12317
- function readLiveCustomId(edit, deps) {
12318
- if (edit.column !== CUSTOM_ID_COLUMN) return null;
12319
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12320
- const rawValue = liveActivity ? liveActivity.customId : null;
12321
- return typeof rawValue === "string" ? rawValue : null;
12775
+ seenIds.add(id);
12776
+ seenBackendIds.add(assignment.proplannerId);
12322
12777
  }
12323
- function syncCustomIdTracker(edit, oldCustomId, deps) {
12324
- if (edit.column !== CUSTOM_ID_COLUMN) return;
12325
- const liveActivity = deps.adapter.getActivity(edit.activityId);
12326
- const rawValue = liveActivity ? liveActivity.customId : null;
12327
- const newCustomId = typeof rawValue === "string" ? rawValue : null;
12328
- if (oldCustomId !== newCustomId) {
12329
- deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12778
+ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
12779
+ const seenIds = /* @__PURE__ */ new Set();
12780
+ const seenBackendIds = /* @__PURE__ */ new Set();
12781
+ for (const assignment of assignments) {
12782
+ assertValidAssignment(
12783
+ assignment,
12784
+ entityName,
12785
+ readCurrent,
12786
+ ownerByBackendId,
12787
+ seenIds,
12788
+ seenBackendIds
12789
+ );
12330
12790
  }
12331
12791
  }
12332
- function recordConstraintPrior(priors, edit, beforeSnap) {
12333
- if (!isConstraintEditColumn(edit.column)) return;
12334
- const activityKey = String(edit.activityId);
12335
- if (priors.has(activityKey)) return;
12336
- const preBatchSnapshot = beforeSnap.get(activityKey);
12337
- if (!preBatchSnapshot) return;
12338
- priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12339
- }
12340
- function mergeBeforeSnapshots(beforeSnap, ids, deps) {
12341
- const missing = /* @__PURE__ */ new Set();
12342
- for (const candidateId of ids) {
12343
- if (!beforeSnap.has(candidateId)) missing.add(candidateId);
12344
- }
12345
- if (missing.size === 0) return;
12346
- for (const missingId of missing) {
12347
- const liveActivity = deps.adapter.getActivity(missingId);
12348
- if (liveActivity) {
12349
- beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
12792
+ function assertValidReconciledProgress(entries, deps) {
12793
+ const seen = /* @__PURE__ */ new Set();
12794
+ for (const entry of entries) {
12795
+ const id = String(entry.activityId);
12796
+ if (id.length === 0 || seen.has(id)) {
12797
+ throw new Error(
12798
+ `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
12799
+ );
12800
+ }
12801
+ if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
12802
+ throw new Error(
12803
+ `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
12804
+ );
12805
+ }
12806
+ if (!deps.adapter.getActivity(id)) {
12807
+ throw new Error(
12808
+ `persistence-acknowledge: unknown reconciled activity id "${id}"`
12809
+ );
12350
12810
  }
12811
+ seen.add(id);
12351
12812
  }
12352
12813
  }
12353
-
12354
- // src/dispatch/handlers/sir-approve.ts
12355
- async function dispatchSirApprove(action, options, deps) {
12356
- const { adapter } = deps;
12357
- const snapshot = adapter.getActivity(action.activityId);
12358
- if (!snapshot) {
12359
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
12360
- }
12361
- const pending = snapshot.pendingRequestIds ?? [];
12362
- if (!pending.includes(action.approvedRequestId)) {
12363
- return { ok: false, reason: REJECTION_REASON.SIR_REQUEST_NOT_PENDING };
12814
+ async function applyReconciledProgress(entries, deps) {
12815
+ const progressToApply = entries.filter((entry) => {
12816
+ const current = deps.adapter.getActivity(String(entry.activityId));
12817
+ return current !== null && current.progress !== entry.progress;
12818
+ });
12819
+ const progressIds = new Set(
12820
+ progressToApply.map((entry) => String(entry.activityId))
12821
+ );
12822
+ const ancestors = findAncestorsOfDirty(deps.adapter, progressIds);
12823
+ const touched = new Set(progressIds);
12824
+ for (const parentId of ancestors) {
12825
+ touched.add(String(parentId));
12364
12826
  }
12365
- const remaining = pending.filter((id) => id !== action.approvedRequestId);
12366
- adapter.setActivityField(action.activityId, "pendingRequestIds", remaining);
12367
- const result = await dispatchBulkEdit(
12368
- {
12369
- kind: "bulk-edit",
12370
- edits: [
12371
- {
12372
- activityId: action.activityId,
12373
- column: COLUMN.START_DATE,
12374
- newValue: action.startDate
12375
- },
12376
- {
12377
- activityId: action.activityId,
12378
- column: COLUMN.END_DATE,
12379
- newValue: action.endDate
12380
- },
12381
- // asap ÚLTIMO: gana al SNET implícito (paridad legacy).
12382
- {
12383
- activityId: action.activityId,
12384
- column: COLUMN.CONSTRAINT_TYPE,
12385
- newValue: "asap"
12386
- }
12387
- ],
12388
- eventSource: action.eventSource
12827
+ const before = await snapshotActivities(deps.adapter, touched);
12828
+ deps.adapter.batchUpdate(() => {
12829
+ for (const entry of progressToApply) {
12830
+ deps.adapter.setActivityField(
12831
+ String(entry.activityId),
12832
+ "progress",
12833
+ entry.progress
12834
+ );
12835
+ }
12836
+ recomputeProgressRollupForParents(ancestors, deps.adapter);
12837
+ recomputeRealWorkFromProgress(progressIds, ancestors, deps.adapter);
12838
+ });
12839
+ return [...await buildActivityChanges(deps.adapter, before, touched)];
12840
+ }
12841
+ async function dispatchPersistenceAcknowledge(action, deps) {
12842
+ const activities = action.activities ?? [];
12843
+ const links = action.links ?? [];
12844
+ const reconciledProgress = action.reconciledProgress ?? [];
12845
+ assertValidReconciledProgress(reconciledProgress, deps);
12846
+ const currentActivities = deps.adapter.getAllActivities();
12847
+ const currentLinks = deps.adapter.getAllLinks();
12848
+ const activityOwnerByBackendId = persistedOwnersOf(currentActivities);
12849
+ const linkOwnerByBackendId = persistedOwnersOf(currentLinks);
12850
+ assertValidAssignments(
12851
+ activities,
12852
+ "activity",
12853
+ (id) => {
12854
+ const activity = deps.adapter.getActivity(id);
12855
+ return {
12856
+ exists: activity !== null,
12857
+ current: activity?.proplannerId ?? null
12858
+ };
12389
12859
  },
12390
- options,
12860
+ activityOwnerByBackendId
12861
+ );
12862
+ assertValidAssignments(
12863
+ links,
12864
+ "link",
12865
+ (id) => {
12866
+ const link = deps.adapter.getLink(id);
12867
+ return { exists: link !== null, current: link?.proplannerId ?? null };
12868
+ },
12869
+ linkOwnerByBackendId
12870
+ );
12871
+ assertAllNewEntitiesAcknowledged(currentActivities, activities, "activity");
12872
+ assertAllNewEntitiesAcknowledged(currentLinks, links, "link");
12873
+ const activityChanges = await applyReconciledProgress(
12874
+ reconciledProgress,
12391
12875
  deps
12392
12876
  );
12393
- if (!result.ok) return result;
12394
- const hardFailure = "verdicts" in result ? result.verdicts.find(
12395
- (verdict) => !verdict.ok && verdict.reason !== VALIDATION_REASON.UNCHANGED
12396
- ) : void 0;
12397
- if (hardFailure) {
12398
- return { ok: false, reason: hardFailure.reason };
12399
- }
12400
- return { ...result, changes: { ...result.changes, source: action } };
12877
+ const linkChanges = [];
12878
+ deps.adapter.batchUpdate(() => {
12879
+ for (const assignment of activities) {
12880
+ const before = deps.adapter.getActivity(assignment.id);
12881
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12882
+ const previousProplannerId = before.proplannerId;
12883
+ deps.adapter.setActivityField(
12884
+ assignment.id,
12885
+ "proplannerId",
12886
+ assignment.proplannerId
12887
+ );
12888
+ activityChanges.push({
12889
+ id: assignment.id,
12890
+ kind: "updated",
12891
+ fields: {
12892
+ proplannerId: {
12893
+ before: previousProplannerId,
12894
+ after: assignment.proplannerId
12895
+ }
12896
+ },
12897
+ after: { ...before, proplannerId: assignment.proplannerId }
12898
+ });
12899
+ }
12900
+ for (const assignment of links) {
12901
+ const before = deps.adapter.getLink(assignment.id);
12902
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12903
+ const previousProplannerId = before.proplannerId;
12904
+ deps.adapter.setLinkProplannerId(assignment.id, assignment.proplannerId);
12905
+ linkChanges.push({
12906
+ id: assignment.id,
12907
+ kind: "updated",
12908
+ fields: {
12909
+ proplannerId: {
12910
+ before: previousProplannerId,
12911
+ after: assignment.proplannerId
12912
+ }
12913
+ },
12914
+ after: { ...before, proplannerId: assignment.proplannerId }
12915
+ });
12916
+ }
12917
+ });
12918
+ return {
12919
+ ok: true,
12920
+ changes: {
12921
+ source: action,
12922
+ activities: activityChanges,
12923
+ links: linkChanges,
12924
+ calendars: [],
12925
+ trackingEvents: []
12926
+ }
12927
+ };
12401
12928
  }
12402
12929
 
12403
12930
  // src/dispatch/handlers/activity-lookahead-sync.ts
12404
- function dispatchActivityLookaheadSync(action, deps) {
12931
+ async function dispatchActivityLookaheadSync(action, options, deps) {
12932
+ if (hasInvalidPersistedProgress(action)) {
12933
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
12934
+ }
12405
12935
  const ids = [...new Set(action.activityIds.map(String))];
12936
+ const acknowledgesUnknownPersistedActivity = action.acknowledgePersistedLookahead === true && ids.some((activityId) => !deps.isActivityIdReserved(activityId));
12937
+ if (acknowledgesUnknownPersistedActivity) {
12938
+ return { ok: false, reason: REJECTION_REASON.INVALID };
12939
+ }
12406
12940
  const snapshots = ids.map((id) => deps.adapter.getActivity(id));
12407
12941
  if (snapshots.some((snapshot) => !snapshot)) {
12408
12942
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
@@ -12442,6 +12976,39 @@ function dispatchActivityLookaheadSync(action, deps) {
12442
12976
  }
12443
12977
  });
12444
12978
  }
12979
+ if (action.persistedProgress !== void 0) {
12980
+ const progressChanges = await applyReconciledProgress(
12981
+ [{ activityId: ids[0], progress: action.persistedProgress }],
12982
+ deps
12983
+ );
12984
+ if (progressChanges.length > 0) {
12985
+ await runPostMutation(
12986
+ {
12987
+ adapter: deps.adapter,
12988
+ scheduler: deps.scheduler,
12989
+ sector: deps.sector,
12990
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
12991
+ },
12992
+ {
12993
+ action,
12994
+ autoscheduleFrom: null,
12995
+ recomputeParentsFrom: [],
12996
+ now: deps.now,
12997
+ options
12998
+ }
12999
+ );
13000
+ return {
13001
+ ok: true,
13002
+ changes: await assembleChangeSet(deps.adapter, {
13003
+ source: action,
13004
+ beforeSnap: /* @__PURE__ */ new Map(),
13005
+ touchedIds: [],
13006
+ scheduledIds: [],
13007
+ hoursPerDay: deps.sector.hoursPerDay
13008
+ })
13009
+ };
13010
+ }
13011
+ }
12445
13012
  return {
12446
13013
  ok: true,
12447
13014
  changes: {
@@ -12453,6 +13020,10 @@ function dispatchActivityLookaheadSync(action, deps) {
12453
13020
  }
12454
13021
  };
12455
13022
  }
13023
+ function hasInvalidPersistedProgress(action) {
13024
+ const progress = action.persistedProgress;
13025
+ return progress !== void 0 && (action.activityIds.length !== 1 || !Number.isFinite(progress) || progress < 0 || progress > 100);
13026
+ }
12456
13027
 
12457
13028
  // src/internal/hierarchy/invalid-links.ts
12458
13029
  function collectInvalidLinkIdsForCycle(movedActivityIds, adapter) {
@@ -12720,7 +13291,6 @@ async function dispatchActivityMove(action, options, deps) {
12720
13291
  touchedIds: touched,
12721
13292
  scheduledIds,
12722
13293
  sirDetection: "before-diff",
12723
- preview: options.preview,
12724
13294
  trackingEvents: [trackingEvent],
12725
13295
  hoursPerDay: sector.hoursPerDay
12726
13296
  })
@@ -13218,44 +13788,205 @@ async function dispatchActivitySetProgress(action, options, deps) {
13218
13788
  changes.postProcessors ?? [],
13219
13789
  adapter
13220
13790
  );
13221
- const { scheduledIds } = await runPostMutation(
13222
- {
13223
- adapter,
13224
- scheduler,
13225
- sector: deps.sector,
13226
- defaultBaseCalendarId: deps.defaultBaseCalendarId
13227
- },
13228
- {
13229
- action,
13230
- autoscheduleFrom: changes.needsAutoSchedule ? action.activityId : null,
13231
- recomputeParentsFrom: [],
13232
- collectDirty: (sids) => collectDirtyForInlineEdit(action.activityId, touchedIds, sids),
13233
- now: deps.now,
13234
- options
13791
+ const { scheduledIds } = await runPostMutation(
13792
+ {
13793
+ adapter,
13794
+ scheduler,
13795
+ sector: deps.sector,
13796
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
13797
+ },
13798
+ {
13799
+ action,
13800
+ autoscheduleFrom: changes.needsAutoSchedule ? action.activityId : null,
13801
+ recomputeParentsFrom: [],
13802
+ collectDirty: (sids) => collectDirtyForInlineEdit(action.activityId, touchedIds, sids),
13803
+ now: deps.now,
13804
+ options
13805
+ }
13806
+ );
13807
+ const trackingEvents = (changes.trackingEvents ?? []).map((e) => ({
13808
+ name: e.name,
13809
+ properties: e.properties ?? {}
13810
+ }));
13811
+ return {
13812
+ ok: true,
13813
+ changes: await assembleChangeSet(adapter, {
13814
+ source: action,
13815
+ beforeSnap,
13816
+ touchedIds,
13817
+ scheduledIds,
13818
+ trackingEvents,
13819
+ hoursPerDay: sector.hoursPerDay
13820
+ })
13821
+ };
13822
+ }
13823
+
13824
+ // src/dispatch/handlers/dates-batch.ts
13825
+ async function dispatchDatesBatch(action, options, deps) {
13826
+ const { adapter, scheduler, sector } = deps;
13827
+ const verdicts = [];
13828
+ const beforeSnap = /* @__PURE__ */ new Map();
13829
+ const touchedIds = /* @__PURE__ */ new Set();
13830
+ const trackingEvents = [];
13831
+ const linkChanges = [];
13832
+ const warnings = [];
13833
+ let needsAutoSchedule = false;
13834
+ for (const edit of action.edits) {
13835
+ const outcome = applyOneEdit(edit, deps);
13836
+ verdicts.push(outcome.verdict);
13837
+ if (!outcome.verdict.ok || !outcome.changes) continue;
13838
+ const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
13839
+ mergeBeforeSnapshots(beforeSnap, editTouched, deps);
13840
+ for (const id of editTouched) touchedIds.add(id);
13841
+ const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
13842
+ const preEditSnapshot = snapshotSingleActivity(
13843
+ adapter,
13844
+ String(edit.activityId)
13845
+ );
13846
+ applyFieldChanges(adapter, edit.activityId, outcome.changes);
13847
+ runPostProcessorsOnAdapter(
13848
+ edit.activityId,
13849
+ outcome.changes.postProcessors ?? [],
13850
+ adapter,
13851
+ preEditSnapshot
13852
+ );
13853
+ linkChanges.push(
13854
+ ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
13855
+ );
13856
+ needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
13857
+ for (const e of outcome.changes.trackingEvents ?? []) {
13858
+ trackingEvents.push({
13859
+ name: e.name,
13860
+ properties: e.properties ?? {}
13861
+ });
13862
+ }
13863
+ const constraintWarning = toConstraintWarning(
13864
+ edit.activityId,
13865
+ outcome.changes.constraintWarning
13866
+ );
13867
+ if (constraintWarning) warnings.push(constraintWarning);
13868
+ }
13869
+ const { scheduledIds } = await runPostMutation(
13870
+ {
13871
+ adapter,
13872
+ scheduler,
13873
+ sector: deps.sector,
13874
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
13875
+ },
13876
+ {
13877
+ action,
13878
+ autoscheduleFrom: needsAutoSchedule ? "roots" : null,
13879
+ recomputeParentsFrom: touchedIds,
13880
+ now: deps.now,
13881
+ options
13882
+ }
13883
+ );
13884
+ return {
13885
+ ok: true,
13886
+ verdicts,
13887
+ changes: await assembleChangeSet(adapter, {
13888
+ source: action,
13889
+ beforeSnap,
13890
+ touchedIds,
13891
+ scheduledIds,
13892
+ sirDetection: "before-diff",
13893
+ links: linkChanges,
13894
+ trackingEvents,
13895
+ warnings,
13896
+ hoursPerDay: sector.hoursPerDay
13897
+ })
13898
+ };
13899
+ }
13900
+ function applyOneEdit(edit, deps) {
13901
+ const { ctx } = deps;
13902
+ const activityId = edit.activityId;
13903
+ const pipeline = findPipelineForColumn(edit.column);
13904
+ if (!pipeline) {
13905
+ throw new Error(
13906
+ `[ScheduleCore] no pipeline registered for column "${edit.column}"`
13907
+ );
13908
+ }
13909
+ const activitySnap = ctx.activityReader.getActivity(edit.activityId);
13910
+ if (!activitySnap) {
13911
+ throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
13912
+ }
13913
+ const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
13914
+ if (!editGate.allowed) {
13915
+ return {
13916
+ verdict: {
13917
+ activityId,
13918
+ ok: false,
13919
+ reason: editGate.reason ?? REJECTION_REASON.CANNOT_EDIT,
13920
+ ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
13921
+ },
13922
+ changes: null
13923
+ };
13924
+ }
13925
+ const parsed2 = pipeline.parseInput(edit.newValue, activitySnap, ctx);
13926
+ if (!parsed2.ok) {
13927
+ return {
13928
+ verdict: {
13929
+ activityId,
13930
+ ok: false,
13931
+ reason: parsed2.reason ?? REJECTION_REASON.PARSE_ERROR
13932
+ },
13933
+ changes: null
13934
+ };
13935
+ }
13936
+ const oldValue = Reflect.get(activitySnap, pipeline.targetField);
13937
+ const validation = pipeline.validate(
13938
+ activitySnap,
13939
+ oldValue,
13940
+ parsed2.value,
13941
+ ctx
13942
+ );
13943
+ if (!validation.valid) {
13944
+ return {
13945
+ verdict: {
13946
+ activityId,
13947
+ ok: false,
13948
+ reason: validation.reason ?? REJECTION_REASON.INVALID,
13949
+ ...validation.notification?.messageKey ? { alertKey: validation.notification.messageKey } : {}
13950
+ },
13951
+ changes: null
13952
+ };
13953
+ }
13954
+ const changes = pipeline.transform(
13955
+ activitySnap,
13956
+ parsed2.value,
13957
+ ctx,
13958
+ parsed2.raw
13959
+ );
13960
+ return { verdict: { activityId, ok: true }, changes };
13961
+ }
13962
+ function mergeBeforeSnapshots(beforeSnap, ids, deps) {
13963
+ const missing = /* @__PURE__ */ new Set();
13964
+ for (const id of ids) {
13965
+ if (!beforeSnap.has(id)) missing.add(id);
13966
+ }
13967
+ if (missing.size === 0) return;
13968
+ for (const missingId of missing) {
13969
+ const liveActivity = deps.adapter.getActivity(missingId);
13970
+ if (liveActivity) {
13971
+ beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
13235
13972
  }
13236
- );
13237
- const trackingEvents = (changes.trackingEvents ?? []).map((e) => ({
13238
- name: e.name,
13239
- properties: e.properties ?? {}
13240
- }));
13241
- return {
13242
- ok: true,
13243
- changes: await assembleChangeSet(adapter, {
13244
- source: action,
13245
- beforeSnap,
13246
- touchedIds,
13247
- scheduledIds,
13248
- trackingEvents,
13249
- hoursPerDay: sector.hoursPerDay
13250
- })
13251
- };
13973
+ }
13252
13974
  }
13253
13975
 
13254
- // src/dispatch/handlers/dates-batch.ts
13255
- async function dispatchDatesBatch(action, options, deps) {
13976
+ // src/dispatch/handlers/bulk-edit.ts
13977
+ var CUSTOM_ID_COLUMN = COLUMN.CUSTOM_ID;
13978
+ async function dispatchBulkEdit(action, options, deps) {
13256
13979
  const { adapter, scheduler, sector } = deps;
13980
+ const editsIncludeDuration = action.edits.some(
13981
+ (edit) => edit.column === COLUMN.DURATION
13982
+ );
13983
+ if (editsIncludeDuration) {
13984
+ scheduler.invalidateExpandedLinksCache();
13985
+ }
13257
13986
  const verdicts = [];
13258
13987
  const beforeSnap = /* @__PURE__ */ new Map();
13988
+ const constraintPriorsByActivity = /* @__PURE__ */ new Map();
13989
+ const explicitDateEditActivities = /* @__PURE__ */ new Set();
13259
13990
  const touchedIds = /* @__PURE__ */ new Set();
13260
13991
  const trackingEvents = [];
13261
13992
  const linkChanges = [];
@@ -13265,52 +13996,83 @@ async function dispatchDatesBatch(action, options, deps) {
13265
13996
  const outcome = applyOneEdit2(edit, deps);
13266
13997
  verdicts.push(outcome.verdict);
13267
13998
  if (!outcome.verdict.ok || !outcome.changes) continue;
13999
+ const constraintWarning = toConstraintWarning(
14000
+ edit.activityId,
14001
+ outcome.changes.constraintWarning
14002
+ );
14003
+ if (constraintWarning) warnings.push(constraintWarning);
13268
14004
  const editTouched = collectTouchedIds(edit.activityId, outcome.changes);
13269
14005
  mergeBeforeSnapshots2(beforeSnap, editTouched, deps);
13270
- for (const id of editTouched) touchedIds.add(id);
14006
+ for (const touchedId of editTouched) touchedIds.add(touchedId);
14007
+ recordConstraintPrior(constraintPriorsByActivity, edit, beforeSnap);
14008
+ if (isExplicitConstraintDateEdit(edit.column)) {
14009
+ explicitDateEditActivities.add(String(edit.activityId));
14010
+ }
13271
14011
  const beforeLinkLags = snapshotIncomingLinkLags(adapter, edit.activityId);
13272
14012
  const preEditSnapshot = snapshotSingleActivity(
13273
14013
  adapter,
13274
14014
  String(edit.activityId)
13275
14015
  );
13276
- applyFieldChanges(adapter, edit.activityId, outcome.changes);
14016
+ const customIdBeforeApply = readLiveCustomId(edit, deps);
14017
+ const changes = outcome.changes;
14018
+ applyFieldChanges(adapter, edit.activityId, changes);
13277
14019
  runPostProcessorsOnAdapter(
13278
14020
  edit.activityId,
13279
14021
  outcome.changes.postProcessors ?? [],
13280
14022
  adapter,
13281
14023
  preEditSnapshot
13282
14024
  );
14025
+ syncCustomIdTracker(edit, customIdBeforeApply, deps);
13283
14026
  linkChanges.push(
13284
14027
  ...diffIncomingLinkLagChanges(beforeLinkLags, adapter, edit.activityId)
13285
14028
  );
13286
14029
  needsAutoSchedule ||= Boolean(outcome.changes.needsAutoSchedule);
13287
- for (const e of outcome.changes.trackingEvents ?? []) {
14030
+ for (const trackingEvent of outcome.changes.trackingEvents ?? []) {
13288
14031
  trackingEvents.push({
13289
- name: e.name,
13290
- properties: e.properties ?? {}
14032
+ name: trackingEvent.name,
14033
+ properties: trackingEvent.properties ?? {}
13291
14034
  });
13292
14035
  }
13293
- const constraintWarning = toConstraintWarning(
13294
- edit.activityId,
13295
- outcome.changes.constraintWarning
13296
- );
13297
- if (constraintWarning) warnings.push(constraintWarning);
13298
14036
  }
13299
- const { scheduledIds } = await runPostMutation(
14037
+ const postMutationArgs = {
14038
+ action,
14039
+ autoscheduleFrom: needsAutoSchedule ? "roots" : null,
14040
+ recomputeParentsFrom: touchedIds,
14041
+ now: deps.now,
14042
+ options
14043
+ };
14044
+ const batchHasConstraintEdits = constraintPriorsByActivity.size > 0;
14045
+ const { scheduledIds } = await (batchHasConstraintEdits ? settleConstraintEdits(
13300
14046
  {
13301
14047
  adapter,
13302
14048
  scheduler,
13303
- sector: deps.sector,
14049
+ sector,
13304
14050
  defaultBaseCalendarId: deps.defaultBaseCalendarId
13305
14051
  },
13306
14052
  {
13307
- action,
13308
- autoscheduleFrom: needsAutoSchedule ? "roots" : null,
13309
- recomputeParentsFrom: touchedIds,
13310
- now: deps.now,
13311
- options
14053
+ postMutationArgs,
14054
+ revertTargets: [...constraintPriorsByActivity].filter(
14055
+ ([activityId]) => !explicitDateEditActivities.has(activityId)
14056
+ ).map(([activityId, priorConstraintDate]) => ({
14057
+ activityId,
14058
+ priorConstraintDate
14059
+ })),
14060
+ revert: (activityId, priorConstraintDate) => revertNoOpConstraintEdit(
14061
+ activityId,
14062
+ priorConstraintDate,
14063
+ adapter,
14064
+ deps.ctx.calendars
14065
+ )
13312
14066
  }
13313
- );
14067
+ ) : runPostMutation(
14068
+ {
14069
+ adapter,
14070
+ scheduler,
14071
+ sector,
14072
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
14073
+ },
14074
+ postMutationArgs
14075
+ ));
13314
14076
  return {
13315
14077
  ok: true,
13316
14078
  verdicts,
@@ -13320,7 +14082,6 @@ async function dispatchDatesBatch(action, options, deps) {
13320
14082
  touchedIds,
13321
14083
  scheduledIds,
13322
14084
  sirDetection: "before-diff",
13323
- preview: options.preview,
13324
14085
  links: linkChanges,
13325
14086
  trackingEvents,
13326
14087
  warnings,
@@ -13333,13 +14094,25 @@ function applyOneEdit2(edit, deps) {
13333
14094
  const activityId = edit.activityId;
13334
14095
  const pipeline = findPipelineForColumn(edit.column);
13335
14096
  if (!pipeline) {
13336
- throw new Error(
13337
- `[ScheduleCore] no pipeline registered for column "${edit.column}"`
13338
- );
14097
+ return {
14098
+ verdict: {
14099
+ activityId,
14100
+ ok: false,
14101
+ reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN
14102
+ },
14103
+ changes: null
14104
+ };
13339
14105
  }
13340
14106
  const activitySnap = ctx.activityReader.getActivity(edit.activityId);
13341
14107
  if (!activitySnap) {
13342
- throw new Error(`[ScheduleCore] activity "${edit.activityId}" not found`);
14108
+ return {
14109
+ verdict: {
14110
+ activityId,
14111
+ ok: false,
14112
+ reason: REJECTION_REASON.ACTIVITY_NOT_FOUND
14113
+ },
14114
+ changes: null
14115
+ };
13343
14116
  }
13344
14117
  const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
13345
14118
  if (!editGate.allowed) {
@@ -13382,6 +14155,21 @@ function applyOneEdit2(edit, deps) {
13382
14155
  changes: null
13383
14156
  };
13384
14157
  }
14158
+ if (edit.column === CUSTOM_ID_COLUMN && parsed2.value !== null) {
14159
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
14160
+ const oldCustomIdRaw = liveActivity ? liveActivity.customId : null;
14161
+ const oldCustomId = typeof oldCustomIdRaw === "string" ? oldCustomIdRaw : null;
14162
+ if (deps.customIdTracker.isCustomIdInUse(String(parsed2.value), oldCustomId)) {
14163
+ return {
14164
+ verdict: {
14165
+ activityId,
14166
+ ok: false,
14167
+ reason: REJECTION_REASON.CUSTOM_ID_DUPLICATE
14168
+ },
14169
+ changes: null
14170
+ };
14171
+ }
14172
+ }
13385
14173
  const changes = pipeline.transform(
13386
14174
  activitySnap,
13387
14175
  parsed2.value,
@@ -13390,226 +14178,41 @@ function applyOneEdit2(edit, deps) {
13390
14178
  );
13391
14179
  return { verdict: { activityId, ok: true }, changes };
13392
14180
  }
13393
- function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
13394
- const missing = /* @__PURE__ */ new Set();
13395
- for (const id of ids) {
13396
- if (!beforeSnap.has(id)) missing.add(id);
13397
- }
13398
- if (missing.size === 0) return;
13399
- for (const missingId of missing) {
13400
- const liveActivity = deps.adapter.getActivity(missingId);
13401
- if (liveActivity) {
13402
- beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
13403
- }
13404
- }
13405
- }
13406
-
13407
- // src/dispatch/handlers/persistence-acknowledge.ts
13408
- function assertValidAssignment(assignment, entityName, readCurrent, ownerByBackendId, seenIds, seenBackendIds) {
13409
- const id = String(assignment.id);
13410
- if (id.length === 0 || seenIds.has(id)) {
13411
- throw new Error(
13412
- `persistence-acknowledge: duplicate or empty ${entityName} id "${id}"`
13413
- );
13414
- }
13415
- if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0) {
13416
- throw new Error(
13417
- `persistence-acknowledge: invalid ${entityName} proplannerId ${String(assignment.proplannerId)}`
13418
- );
13419
- }
13420
- if (seenBackendIds.has(assignment.proplannerId)) {
13421
- throw new Error(
13422
- `persistence-acknowledge: duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
13423
- );
13424
- }
13425
- const state = readCurrent(id);
13426
- if (!state.exists) {
13427
- throw new Error(
13428
- `persistence-acknowledge: unknown ${entityName} id "${id}"`
13429
- );
13430
- }
13431
- if (state.current !== null && state.current !== assignment.proplannerId) {
13432
- throw new Error(
13433
- `persistence-acknowledge: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
13434
- );
13435
- }
13436
- const existingOwner = ownerByBackendId.get(assignment.proplannerId);
13437
- if (existingOwner !== void 0 && existingOwner !== id) {
13438
- throw new Error(
13439
- `persistence-acknowledge: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
13440
- );
13441
- }
13442
- seenIds.add(id);
13443
- seenBackendIds.add(assignment.proplannerId);
13444
- }
13445
- function assertValidAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
13446
- const seenIds = /* @__PURE__ */ new Set();
13447
- const seenBackendIds = /* @__PURE__ */ new Set();
13448
- for (const assignment of assignments) {
13449
- assertValidAssignment(
13450
- assignment,
13451
- entityName,
13452
- readCurrent,
13453
- ownerByBackendId,
13454
- seenIds,
13455
- seenBackendIds
13456
- );
13457
- }
14181
+ function readLiveCustomId(edit, deps) {
14182
+ if (edit.column !== CUSTOM_ID_COLUMN) return null;
14183
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
14184
+ const rawValue = liveActivity ? liveActivity.customId : null;
14185
+ return typeof rawValue === "string" ? rawValue : null;
13458
14186
  }
13459
- function assertValidReconciledProgress(entries, deps) {
13460
- const seen = /* @__PURE__ */ new Set();
13461
- for (const entry of entries) {
13462
- const id = String(entry.activityId);
13463
- if (id.length === 0 || seen.has(id)) {
13464
- throw new Error(
13465
- `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
13466
- );
13467
- }
13468
- if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
13469
- throw new Error(
13470
- `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
13471
- );
13472
- }
13473
- if (!deps.adapter.getActivity(id)) {
13474
- throw new Error(
13475
- `persistence-acknowledge: unknown reconciled activity id "${id}"`
13476
- );
13477
- }
13478
- seen.add(id);
14187
+ function syncCustomIdTracker(edit, oldCustomId, deps) {
14188
+ if (edit.column !== CUSTOM_ID_COLUMN) return;
14189
+ const liveActivity = deps.adapter.getActivity(edit.activityId);
14190
+ const rawValue = liveActivity ? liveActivity.customId : null;
14191
+ const newCustomId = typeof rawValue === "string" ? rawValue : null;
14192
+ if (oldCustomId !== newCustomId) {
14193
+ deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
13479
14194
  }
13480
14195
  }
13481
- async function dispatchPersistenceAcknowledge(action, deps) {
13482
- const activities = action.activities ?? [];
13483
- const links = action.links ?? [];
13484
- const reconciledProgress = action.reconciledProgress ?? [];
13485
- assertValidReconciledProgress(reconciledProgress, deps);
13486
- const activityOwnerByBackendId = /* @__PURE__ */ new Map();
13487
- for (const activity of deps.adapter.getAllActivities()) {
13488
- if (activity.proplannerId != null) {
13489
- activityOwnerByBackendId.set(activity.proplannerId, String(activity.id));
13490
- }
13491
- }
13492
- const linkOwnerByBackendId = /* @__PURE__ */ new Map();
13493
- for (const link of deps.adapter.getAllLinks()) {
13494
- if (link.proplannerId != null) {
13495
- linkOwnerByBackendId.set(link.proplannerId, String(link.id));
13496
- }
14196
+ function recordConstraintPrior(priors, edit, beforeSnap) {
14197
+ if (!isConstraintEditColumn(edit.column)) return;
14198
+ const activityKey = String(edit.activityId);
14199
+ if (priors.has(activityKey)) return;
14200
+ const preBatchSnapshot = beforeSnap.get(activityKey);
14201
+ if (!preBatchSnapshot) return;
14202
+ priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
14203
+ }
14204
+ function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
14205
+ const missing = /* @__PURE__ */ new Set();
14206
+ for (const candidateId of ids) {
14207
+ if (!beforeSnap.has(candidateId)) missing.add(candidateId);
13497
14208
  }
13498
- assertValidAssignments(
13499
- activities,
13500
- "activity",
13501
- (id) => {
13502
- const activity = deps.adapter.getActivity(id);
13503
- return {
13504
- exists: activity !== null,
13505
- current: activity?.proplannerId ?? null
13506
- };
13507
- },
13508
- activityOwnerByBackendId
13509
- );
13510
- assertValidAssignments(
13511
- links,
13512
- "link",
13513
- (id) => {
13514
- const link = deps.adapter.getLink(id);
13515
- return { exists: link !== null, current: link?.proplannerId ?? null };
13516
- },
13517
- linkOwnerByBackendId
13518
- );
13519
- const activityChanges = [];
13520
- const linkChanges = [];
13521
- const progressToApply = reconciledProgress.filter((entry) => {
13522
- const current = deps.adapter.getActivity(String(entry.activityId));
13523
- return current !== null && current.progress !== entry.progress;
13524
- });
13525
- const touchedForProgress = new Set(
13526
- progressToApply.map((entry) => String(entry.activityId))
13527
- );
13528
- const parentsToRollup = /* @__PURE__ */ new Set();
13529
- for (const entry of progressToApply) {
13530
- const activity = deps.adapter.getActivity(String(entry.activityId));
13531
- if (activity && !isRootParent(activity.parentId)) {
13532
- parentsToRollup.add(String(activity.parentId));
13533
- touchedForProgress.add(String(activity.parentId));
14209
+ if (missing.size === 0) return;
14210
+ for (const missingId of missing) {
14211
+ const liveActivity = deps.adapter.getActivity(missingId);
14212
+ if (liveActivity) {
14213
+ beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
13534
14214
  }
13535
14215
  }
13536
- const beforeProgressSnap = await snapshotActivities(
13537
- deps.adapter,
13538
- touchedForProgress
13539
- );
13540
- deps.adapter.batchUpdate(() => {
13541
- for (const entry of progressToApply) {
13542
- deps.adapter.setActivityField(
13543
- String(entry.activityId),
13544
- "progress",
13545
- entry.progress
13546
- );
13547
- }
13548
- for (const parentId of parentsToRollup) {
13549
- recomputeRollupCascadesForParent(parentId, deps.adapter);
13550
- }
13551
- for (const id of touchedForProgress) {
13552
- const before = beforeProgressSnap.get(id);
13553
- const after = deps.adapter.getActivity(id);
13554
- if (!before || !after || before.progress === after.progress) continue;
13555
- activityChanges.push({
13556
- id,
13557
- kind: "updated",
13558
- fields: {
13559
- progress: { before: before.progress, after: after.progress }
13560
- },
13561
- after: { ...after }
13562
- });
13563
- }
13564
- for (const assignment of activities) {
13565
- const before = deps.adapter.getActivity(assignment.id);
13566
- if (!before || before.proplannerId === assignment.proplannerId) continue;
13567
- const previousProplannerId = before.proplannerId;
13568
- deps.adapter.setActivityField(
13569
- assignment.id,
13570
- "proplannerId",
13571
- assignment.proplannerId
13572
- );
13573
- activityChanges.push({
13574
- id: assignment.id,
13575
- kind: "updated",
13576
- fields: {
13577
- proplannerId: {
13578
- before: previousProplannerId,
13579
- after: assignment.proplannerId
13580
- }
13581
- },
13582
- after: { ...before, proplannerId: assignment.proplannerId }
13583
- });
13584
- }
13585
- for (const assignment of links) {
13586
- const before = deps.adapter.getLink(assignment.id);
13587
- if (!before || before.proplannerId === assignment.proplannerId) continue;
13588
- const previousProplannerId = before.proplannerId;
13589
- deps.adapter.setLinkProplannerId(assignment.id, assignment.proplannerId);
13590
- linkChanges.push({
13591
- id: assignment.id,
13592
- kind: "updated",
13593
- fields: {
13594
- proplannerId: {
13595
- before: previousProplannerId,
13596
- after: assignment.proplannerId
13597
- }
13598
- },
13599
- after: { ...before, proplannerId: assignment.proplannerId }
13600
- });
13601
- }
13602
- });
13603
- return {
13604
- ok: true,
13605
- changes: {
13606
- source: action,
13607
- activities: activityChanges,
13608
- links: linkChanges,
13609
- calendars: [],
13610
- trackingEvents: []
13611
- }
13612
- };
13613
14216
  }
13614
14217
 
13615
14218
  // src/baselines/normalize-baseline-point.ts
@@ -13807,55 +14410,6 @@ function recomputeAllPonderators(criterion, adapter) {
13807
14410
  });
13808
14411
  }
13809
14412
 
13810
- // src/propagations/upward/progress-rollup.ts
13811
- function recomputeAllProgressRollup(adapter) {
13812
- for (const parentId of parentsDeepestFirst(adapter)) {
13813
- rollupParent(parentId, adapter);
13814
- }
13815
- }
13816
- function parentsDeepestFirst(adapter) {
13817
- const parents = /* @__PURE__ */ new Set();
13818
- adapter.forEachActivity((_activity, id) => {
13819
- if (adapter.getChildren(id).length > 0) parents.add(id);
13820
- });
13821
- const depthById = /* @__PURE__ */ new Map();
13822
- for (const id of parents) depthById.set(id, depthOf2(id, adapter));
13823
- return [...parents].sort(
13824
- (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
13825
- );
13826
- }
13827
- function depthOf2(id, adapter) {
13828
- let depth = 0;
13829
- let current = id;
13830
- while (current && current !== "0") {
13831
- if (!adapter.getActivity(current)) break;
13832
- depth += 1;
13833
- current = adapter.getParentId(current);
13834
- }
13835
- return depth;
13836
- }
13837
- function rollupParent(parentId, adapter) {
13838
- const childIds = adapter.getChildren(parentId);
13839
- if (childIds.length === 0) return;
13840
- const children = [];
13841
- for (const childId of childIds) {
13842
- const child = adapter.getActivity(childId);
13843
- if (!child) continue;
13844
- children.push({
13845
- progress: Number(child.progress ?? 0),
13846
- ponderator: Number(child.ponderator ?? 0)
13847
- });
13848
- }
13849
- const rollup = computeWeightedProgressRollup(children);
13850
- if (rollup !== null) {
13851
- adapter.setActivityField(
13852
- parentId,
13853
- COLUMN.PROGRESS,
13854
- roundProgressPerLevel(rollup)
13855
- );
13856
- }
13857
- }
13858
-
13859
14413
  // src/dispatch/shared/baseline-weighted-fields.ts
13860
14414
  function recomputeBaselineWeightedFields(deps, criterion) {
13861
14415
  recomputeAllPonderators(criterion, deps.adapter);
@@ -14245,11 +14799,8 @@ async function performDispatch(action, options, deps) {
14245
14799
  if (action.kind === "sir-sync") {
14246
14800
  return dispatchSirSync(action, deps);
14247
14801
  }
14248
- if (action.kind === "sir-approve") {
14249
- return dispatchSirApprove(action, options, deps);
14250
- }
14251
14802
  if (action.kind === "activity-lookahead-sync") {
14252
- return dispatchActivityLookaheadSync(action, deps);
14803
+ return dispatchActivityLookaheadSync(action, options, deps);
14253
14804
  }
14254
14805
  if (action.kind === "inline-edit" && (action.column === COLUMN.CUSTOM_PREDECESSORS || action.column === COLUMN.CUSTOM_SUCESSORS)) {
14255
14806
  return dispatchInlineEditLinks(action, options, deps);
@@ -17327,79 +17878,8 @@ var HierarchyIndex = class {
17327
17878
  }
17328
17879
  };
17329
17880
 
17330
- // src/internal/state/view-state.ts
17331
- var ViewStateStore = class {
17332
- checkedSet = /* @__PURE__ */ new Set();
17333
- hiddenSet = /* @__PURE__ */ new Set();
17334
- activeFilter = null;
17335
- activeOrder = null;
17336
- isChecked(activityId) {
17337
- return this.checkedSet.has(activityId);
17338
- }
17339
- isVisible(activityId) {
17340
- return !this.hiddenSet.has(activityId);
17341
- }
17342
- hiddenIds() {
17343
- return [...this.hiddenSet];
17344
- }
17345
- getActiveFilter() {
17346
- return this.activeFilter;
17347
- }
17348
- setActiveFilter(nextFilter) {
17349
- this.activeFilter = nextFilter;
17350
- }
17351
- getActiveOrder() {
17352
- return this.activeOrder;
17353
- }
17354
- setActiveOrder(nextOrder) {
17355
- this.activeOrder = nextOrder;
17356
- }
17357
- checkedIds() {
17358
- return [...this.checkedSet];
17359
- }
17360
- checkedCount() {
17361
- return this.checkedSet.size;
17362
- }
17363
- setChecked(activityId, nextChecked) {
17364
- if (nextChecked === this.checkedSet.has(activityId)) return false;
17365
- if (nextChecked) this.checkedSet.add(activityId);
17366
- else this.checkedSet.delete(activityId);
17367
- return true;
17368
- }
17369
- setVisible(activityId, nextVisible) {
17370
- if (nextVisible === !this.hiddenSet.has(activityId)) return false;
17371
- if (nextVisible) this.hiddenSet.delete(activityId);
17372
- else this.hiddenSet.add(activityId);
17373
- return true;
17374
- }
17375
- forget(activityId) {
17376
- this.checkedSet.delete(activityId);
17377
- this.hiddenSet.delete(activityId);
17378
- }
17379
- hydrate(seed2) {
17380
- this.checkedSet = new Set(seed2.checkedIds ?? []);
17381
- this.hiddenSet = new Set(seed2.hiddenIds ?? []);
17382
- this.activeFilter = null;
17383
- this.activeOrder = null;
17384
- }
17385
- snapshot() {
17386
- return {
17387
- checked: new Set(this.checkedSet),
17388
- hidden: new Set(this.hiddenSet),
17389
- activeFilter: this.activeFilter,
17390
- activeOrder: this.activeOrder
17391
- };
17392
- }
17393
- restore(snapshot) {
17394
- this.checkedSet = new Set(snapshot.checked);
17395
- this.hiddenSet = new Set(snapshot.hidden);
17396
- this.activeFilter = snapshot.activeFilter;
17397
- this.activeOrder = snapshot.activeOrder;
17398
- }
17399
- };
17400
-
17401
17881
  // src/internal/state/schedule-state.ts
17402
- var ScheduleState = class {
17882
+ var ScheduleState = class _ScheduleState {
17403
17883
  _activities = /* @__PURE__ */ new Map();
17404
17884
  _lastStartDate = /* @__PURE__ */ new Map();
17405
17885
  _promotionSnapshot = /* @__PURE__ */ new Map();
@@ -17421,22 +17901,52 @@ var ScheduleState = class {
17421
17901
  _promotionSnapshotBefore = null;
17422
17902
  _calendar;
17423
17903
  _flags;
17904
+ _calendarSnapshot;
17424
17905
  constructor(snapshot, buildOrderComparator = () => null) {
17425
17906
  this._buildOrderComparator = buildOrderComparator;
17907
+ this._calendarSnapshot = cloneDomainValue(snapshot.calendars);
17426
17908
  for (const activity of snapshot.activities) {
17427
17909
  this._activities.set(activity.id, activity);
17428
17910
  }
17429
17911
  for (const link of snapshot.links) {
17430
- this._links.set(String(link.id), link);
17431
- this._indexLink(link);
17912
+ const normalized2 = {
17913
+ ...link,
17914
+ freeSlackHours: link.freeSlackHours ?? null,
17915
+ isCritical: link.isCritical === true
17916
+ };
17917
+ this._links.set(String(link.id), normalized2);
17918
+ this._indexLink(normalized2);
17432
17919
  }
17433
17920
  this._calendar = new CalendarCalculator(
17434
- createCalendarSetup(snapshot.calendars)
17921
+ createCalendarSetup(this._calendarSnapshot)
17435
17922
  );
17436
17923
  this._flags = buildFlags(snapshot);
17437
17924
  this._hierarchy = new HierarchyIndex(this._activities);
17438
17925
  this._hierarchy.rebuild();
17439
17926
  }
17927
+ fork() {
17928
+ const fork = new _ScheduleState(
17929
+ {
17930
+ activities: this.getAllActivities().map(cloneCoreActivity),
17931
+ links: this.getAllLinks().map((link) => ({ ...link })),
17932
+ calendars: cloneDomainValue(this._calendarSnapshot)
17933
+ },
17934
+ this._buildOrderComparator
17935
+ );
17936
+ fork._viewState.restore(cloneDomainValue(this._viewState.snapshot()));
17937
+ fork._lastStartDate = new Map(
17938
+ Array.from(this._lastStartDate, ([id, date2]) => [id, new Date(date2)])
17939
+ );
17940
+ fork._promotionSnapshot = new Map(
17941
+ Array.from(this._promotionSnapshot, ([id, snapshot]) => [
17942
+ id,
17943
+ cloneDomainValue(snapshot)
17944
+ ])
17945
+ );
17946
+ fork._flags = cloneDomainValue(this._flags);
17947
+ fork._filterProjection = cloneDomainValue(this._filterProjection);
17948
+ return fork;
17949
+ }
17440
17950
  getAllActivities() {
17441
17951
  return Array.from(this._activities.values());
17442
17952
  }
@@ -17546,6 +18056,18 @@ var ScheduleState = class {
17546
18056
  this._viewState.setActiveOrder(nextOrder);
17547
18057
  this.invalidateDerivedOrder();
17548
18058
  }
18059
+ setRelativePlacementPin(activityId, pin) {
18060
+ this._viewState.setRelativePlacementPin(String(activityId), pin);
18061
+ this.invalidateVisualOrderIds();
18062
+ }
18063
+ getRelativePlacementPins() {
18064
+ return this._viewState.getRelativePlacementPins();
18065
+ }
18066
+ restoreRelativePlacementPins(pins) {
18067
+ this._viewState.restoreRelativePlacementPins(pins);
18068
+ this._filterProjection = null;
18069
+ this.invalidateVisualOrderIds();
18070
+ }
17549
18071
  /**
17550
18072
  * Rebuild-on-demand, memoized: at most one build per invalidation, never one
17551
18073
  * per comparison. getChildrenInVisualOrder asks for this once and then sorts a
@@ -17694,9 +18216,14 @@ var ScheduleState = class {
17694
18216
  addLink(link) {
17695
18217
  this._writeCapture.noteLinkAdd(String(link.id));
17696
18218
  const superseded = this._links.get(String(link.id));
17697
- if (superseded) this._deindexLink(superseded);
17698
- this._links.set(String(link.id), { ...link });
17699
- this._indexLink(link);
18219
+ if (superseded) this._deindexLink(superseded);
18220
+ const normalized2 = {
18221
+ ...link,
18222
+ freeSlackHours: link.freeSlackHours ?? null,
18223
+ isCritical: link.isCritical === true
18224
+ };
18225
+ this._links.set(String(link.id), normalized2);
18226
+ this._indexLink(normalized2);
17700
18227
  }
17701
18228
  _indexLink(link) {
17702
18229
  pushLinkRef(this._outgoing, String(link.source), link.id);
@@ -17747,10 +18274,14 @@ var ScheduleState = class {
17747
18274
  const activity = this.getActivity(activityId);
17748
18275
  if (activity) this._writeCapture.noteRemove(String(activityId), activity);
17749
18276
  const key = String(activityId);
17750
- if (this._viewState.isChecked(key) || !this._viewState.isVisible(key)) {
18277
+ const relativePlacementPins = this._viewState.getRelativePlacementPins();
18278
+ const hasDependentPin = [...relativePlacementPins.values()].some(
18279
+ ({ referenceId }) => referenceId === key
18280
+ );
18281
+ if (this._viewState.isChecked(key) || !this._viewState.isVisible(key) || relativePlacementPins.has(key) || hasDependentPin) {
17751
18282
  this._captureViewStateOnce();
17752
- this._viewState.forget(key);
17753
18283
  }
18284
+ this._viewState.forget(key);
17754
18285
  this._activities.delete(activityId);
17755
18286
  if (this._lastStartDate.has(String(activityId))) {
17756
18287
  this._captureLastStartDateOnce();
@@ -17849,7 +18380,7 @@ var ScheduleState = class {
17849
18380
  };
17850
18381
  var EMPTY_LINK_IDS = [];
17851
18382
  function isMutableLinkField(field) {
17852
- return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE;
18383
+ return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE || field === LINK_PROPERTY.FREE_SLACK_HOURS || field === LINK_PROPERTY.IS_CRITICAL;
17853
18384
  }
17854
18385
  function fieldAffectsVisualOrder(field) {
17855
18386
  return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
@@ -17970,12 +18501,7 @@ function buildPipelineContext(adapter, options) {
17970
18501
  activityReader: createStateBackedActivityReader(adapter),
17971
18502
  dateFormat: options.dateFormat ?? "YYYY-MM-DD",
17972
18503
  customHours: options.customHours ?? {},
17973
- hoursPerDay: options.hoursPerDay,
17974
- // ISSUE-057 (CP6): sin default. Un comando de duración sin inputUnit se
17975
- // rechaza en el pipeline; el resto de columnas no lo leen. La clave se
17976
- // OMITE cuando no se declaró (exactOptionalPropertyTypes), no se pone a
17977
- // undefined.
17978
- ...options.inputUnit !== void 0 ? { inputUnit: options.inputUnit } : {}
18504
+ hoursPerDay: options.hoursPerDay
17979
18505
  };
17980
18506
  }
17981
18507
 
@@ -17985,15 +18511,26 @@ function willRunCriticalPath(action) {
17985
18511
  }
17986
18512
 
17987
18513
  // src/dispatch/undo/undo-entry.ts
17988
- function buildHistoryChangeSet(changes) {
17989
- const activitiesWithoutAfter = changes.activities.map((entry) => ({
18514
+ var DERIVED_LINK_FIELDS = /* @__PURE__ */ new Set(["freeSlackHours", "isCritical"]);
18515
+ function withoutDerivedFields(entry, derivedFields) {
18516
+ const fields = Object.fromEntries(
18517
+ Object.entries(entry.fields ?? {}).filter(
18518
+ ([field]) => !derivedFields.has(field)
18519
+ )
18520
+ );
18521
+ if (entry.kind === "updated" && Object.keys(fields).length === 0) return null;
18522
+ return {
17990
18523
  ...entry,
18524
+ ...entry.fields === void 0 ? {} : { fields },
17991
18525
  after: null
17992
- }));
17993
- const linksWithoutAfter = changes.links.map((entry) => ({
18526
+ };
18527
+ }
18528
+ function buildHistoryChangeSet(changes) {
18529
+ const activitiesWithoutAfter = changes.activities.map((entry) => ({
17994
18530
  ...entry,
17995
18531
  after: null
17996
18532
  }));
18533
+ const linksWithoutAfter = changes.links.map((entry) => withoutDerivedFields(entry, DERIVED_LINK_FIELDS)).filter((entry) => entry !== null);
17997
18534
  const source = changes.source.kind === "activity-batch" ? { ...changes.source, items: [], links: [] } : changes.source;
17998
18535
  return cloneDomainValue({
17999
18536
  ...changes,
@@ -18002,8 +18539,25 @@ function buildHistoryChangeSet(changes) {
18002
18539
  links: linksWithoutAfter
18003
18540
  });
18004
18541
  }
18005
- function buildUndoEntry(changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks) {
18006
- return { changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks };
18542
+ function buildUndoEntry(changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks, relativePlacementPinsBefore, relativePlacementPinsAfter) {
18543
+ return {
18544
+ changeSet,
18545
+ beforeSnap,
18546
+ beforeLinks: resetDerivedLinkSnapshots(beforeLinks),
18547
+ afterSnap,
18548
+ afterLinks: resetDerivedLinkSnapshots(afterLinks),
18549
+ relativePlacementPinsBefore,
18550
+ relativePlacementPinsAfter
18551
+ };
18552
+ }
18553
+ function resetDerivedLinkSnapshots(snapshots) {
18554
+ if (!snapshots) return void 0;
18555
+ return new Map(
18556
+ Array.from(snapshots, ([id, link]) => [
18557
+ id,
18558
+ { ...link, freeSlackHours: null, isCritical: false }
18559
+ ])
18560
+ );
18007
18561
  }
18008
18562
  function captureCreatedSnapshots(state, changeSet) {
18009
18563
  const afterSnap = /* @__PURE__ */ new Map();
@@ -18055,8 +18609,7 @@ function mergeCoalesced(top, next) {
18055
18609
  };
18056
18610
  return buildUndoEntry(changeSet);
18057
18611
  }
18058
- function getDispatchHistoryPolicy(action, options) {
18059
- if (options?.preview === true) return "skip";
18612
+ function getDispatchHistoryPolicy(action) {
18060
18613
  if (action.kind === "persistence-acknowledge") return "clear-on-success";
18061
18614
  if (action.kind === "sir-sync" || action.kind === "activity-lookahead-sync" || action.kind === "baseline-apply" || action.kind === "ponderator-criterion-set" || action.kind === "status-criteria-set" || action.kind === "activity-clipboard-clear" || action.kind === "activity-copy" || action.kind === "selection-toggle" || action.kind === "selection-replace" || action.kind === "visibility-set" || action.kind === "filter-set" || action.kind === "sort-set") {
18062
18615
  return "skip";
@@ -18297,12 +18850,14 @@ function applyChangeSetSide(state, entry, side) {
18297
18850
  );
18298
18851
  }
18299
18852
  }
18853
+ const pins = side === "before" ? entry.relativePlacementPinsBefore : entry.relativePlacementPinsAfter;
18854
+ if (pins !== void 0) state.restoreRelativePlacementPins(pins);
18300
18855
  }
18301
18856
 
18302
18857
  // src/dispatch/undo/undo-recorder.ts
18303
18858
  var DEFAULT_MAX_DEPTH = 100;
18304
18859
  var COALESCE_WINDOW_MS = 600;
18305
- var UndoRecorder = class {
18860
+ var UndoRecorder = class _UndoRecorder {
18306
18861
  constructor(maxDepth = DEFAULT_MAX_DEPTH) {
18307
18862
  this.maxDepth = maxDepth;
18308
18863
  }
@@ -18311,6 +18866,14 @@ var UndoRecorder = class {
18311
18866
  redoStack = [];
18312
18867
  _lastKey = null;
18313
18868
  _lastTime = 0;
18869
+ fork() {
18870
+ const fork = new _UndoRecorder(this.maxDepth);
18871
+ fork.undoStack.push(...cloneDomainValue(this.undoStack));
18872
+ fork.redoStack.push(...cloneDomainValue(this.redoStack));
18873
+ fork._lastKey = this._lastKey;
18874
+ fork._lastTime = this._lastTime;
18875
+ return fork;
18876
+ }
18314
18877
  record(entry, coalesceKey = null, now = 0) {
18315
18878
  const top = this.undoStack[this.undoStack.length - 1];
18316
18879
  if (coalesceKey != null && coalesceKey === this._lastKey && now - this._lastTime <= COALESCE_WINDOW_MS && top !== void 0 && canCoalesce(top, entry)) {
@@ -18434,40 +18997,56 @@ function buildStateSnapshot(activities, links, calendars, baseCalendars = []) {
18434
18997
  }
18435
18998
 
18436
18999
  // src/boundary/save/activity-changes.ts
18437
- var PERSISTABLE_ACTIVITY_FIELDS = [
18438
- "id",
18439
- "proplannerId",
18440
- "parentId",
18441
- "name",
18442
- "description",
18443
- "type",
18444
- "startDate",
18445
- "endDate",
18446
- "durationHours",
18447
- "constraintType",
18448
- "constraintDate",
18449
- "autoScheduling",
18450
- "calendarId",
18451
- "assignedDefaultCalendar",
18452
- "dateOrigin",
18453
- "progress",
18454
- "cost",
18455
- "usedCost",
18456
- "realCost",
18457
- "workHours",
18458
- "realWorkHours",
18459
- "ponderator",
18460
- "hasCustomPonderator",
18461
- "correlativeId",
18462
- "uniqueCorrelativeId",
18463
- "customId",
18464
- "subcontractId",
18465
- "isLookahead",
18466
- "newActivityIds",
18467
- "hasNewActivities",
18468
- "responsableIds",
18469
- "tagIds"
18470
- ];
19000
+ var ACTIVITY_SAVE_FIELD_POLICY = {
19001
+ id: "ignored",
19002
+ proplannerId: "ignored",
19003
+ parentId: "tracked",
19004
+ name: "tracked",
19005
+ description: "tracked",
19006
+ type: "tracked",
19007
+ startDate: "tracked",
19008
+ endDate: "tracked",
19009
+ durationHours: "tracked",
19010
+ constraintType: "tracked",
19011
+ constraintDate: "tracked",
19012
+ autoScheduling: "tracked",
19013
+ calendarId: "tracked",
19014
+ assignedDefaultCalendar: "ignored",
19015
+ dateOrigin: "tracked",
19016
+ progress: "tracked",
19017
+ cost: "tracked",
19018
+ usedCost: "tracked",
19019
+ realCost: "tracked",
19020
+ workHours: "tracked",
19021
+ realWorkHours: "tracked",
19022
+ ponderator: "tracked",
19023
+ hasCustomPonderator: "tracked",
19024
+ correlativeId: "tracked",
19025
+ uniqueCorrelativeId: "tracked",
19026
+ customId: "tracked",
19027
+ subcontractId: "tracked",
19028
+ isLookahead: "tracked",
19029
+ hasLookaheadTasks: "ignored",
19030
+ newActivityIds: "tracked",
19031
+ hasNewActivities: "tracked",
19032
+ isNewActivity: "ignored",
19033
+ pendingRequestIds: "ignored",
19034
+ responsableIds: "tracked",
19035
+ tagIds: "tracked",
19036
+ baselinePoints: "ignored",
19037
+ baselineSnapshot: "ignored",
19038
+ recursiveDurationHours: "ignored",
19039
+ isCritical: "ignored",
19040
+ freeSlackHours: "ignored",
19041
+ calendarDuration: "ignored",
19042
+ expectedProgress: "ignored",
19043
+ expectedProgressBaseline: "ignored",
19044
+ status: "ignored",
19045
+ criticalPath: "ignored"
19046
+ };
19047
+ var PERSISTABLE_ACTIVITY_FIELDS = Object.keys(ACTIVITY_SAVE_FIELD_POLICY).filter((field) => {
19048
+ return ACTIVITY_SAVE_FIELD_POLICY[field] === "tracked";
19049
+ });
18471
19050
  var DATE_FIELDS2 = /* @__PURE__ */ new Set([
18472
19051
  "startDate",
18473
19052
  "endDate",
@@ -18495,7 +19074,6 @@ function snapshotFieldValue(field, value) {
18495
19074
  return value;
18496
19075
  }
18497
19076
  function activityChangedSince(current, saved) {
18498
- if (current.assignedDefaultCalendar) return true;
18499
19077
  for (const field of PERSISTABLE_ACTIVITY_FIELDS) {
18500
19078
  if (!fieldEquals(field, current[field], saved[field])) return true;
18501
19079
  }
@@ -18513,15 +19091,35 @@ function dateEquals(current, saved) {
18513
19091
  return currentTime === savedTime;
18514
19092
  }
18515
19093
  function idArrayEquals(current, saved) {
18516
- const currentIds = Array.isArray(current) ? [...current].map(String).sort() : [];
18517
- const savedIds = Array.isArray(saved) ? [...saved].map(String).sort() : [];
18518
- if (currentIds.length !== savedIds.length) return false;
18519
- return currentIds.every((value, index) => value === savedIds[index]);
19094
+ const currentIds = new Set(Array.isArray(current) ? current.map(String) : []);
19095
+ const savedIds = new Set(Array.isArray(saved) ? saved.map(String) : []);
19096
+ if (currentIds.size !== savedIds.size) return false;
19097
+ for (const currentId of currentIds) {
19098
+ if (!savedIds.has(currentId)) return false;
19099
+ }
19100
+ return true;
18520
19101
  }
18521
19102
 
18522
19103
  // src/boundary/save/link-changes.ts
19104
+ var LINK_SAVE_FIELD_POLICY = {
19105
+ id: "ignored",
19106
+ source: "tracked",
19107
+ target: "tracked",
19108
+ type: "tracked",
19109
+ lag: "tracked",
19110
+ freeSlackHours: "ignored",
19111
+ isCritical: "ignored",
19112
+ ganttId: "ignored",
19113
+ sectorId: "ignored",
19114
+ proplannerId: "ignored"
19115
+ };
19116
+ var TRACKED_LINK_FIELDS = Object.keys(LINK_SAVE_FIELD_POLICY).filter((field) => {
19117
+ return LINK_SAVE_FIELD_POLICY[field] === "tracked";
19118
+ });
18523
19119
  function linkChangedSince(baseline, current) {
18524
- return baseline.source !== current.source || baseline.target !== current.target || baseline.lag !== current.lag || baseline.type !== current.type;
19120
+ return TRACKED_LINK_FIELDS.some(
19121
+ (field) => baseline[field] !== current[field]
19122
+ );
18525
19123
  }
18526
19124
  function checkNoUpdatedLinks(baseline, current) {
18527
19125
  const baselineById = new Map(
@@ -18535,23 +19133,73 @@ function checkNoUpdatedLinks(baseline, current) {
18535
19133
  }
18536
19134
 
18537
19135
  // src/boundary/save/save-tracker.ts
18538
- var SaveTracker = class {
19136
+ function hasPersistedIdentity(proplannerId) {
19137
+ const isNewIdentity = proplannerId == null || proplannerId === 0;
19138
+ if (isNewIdentity) return false;
19139
+ const isValidPersistedIdentity = typeof proplannerId === "number" && Number.isSafeInteger(proplannerId) && proplannerId > 0;
19140
+ if (!isValidPersistedIdentity) {
19141
+ throw new Error(`invalid proplannerId ${String(proplannerId)}`);
19142
+ }
19143
+ return true;
19144
+ }
19145
+ function assertValidPersistedIdentities(activities, links) {
19146
+ for (const activity of activities) {
19147
+ hasPersistedIdentity(activity.proplannerId);
19148
+ }
19149
+ for (const link of links) {
19150
+ hasPersistedIdentity(link.proplannerId);
19151
+ }
19152
+ }
19153
+ var SaveTracker = class _SaveTracker {
19154
+ trackingEnabled;
18539
19155
  initialized = false;
18540
19156
  dirtyActivityIds = /* @__PURE__ */ new Set();
18541
19157
  dirtyLinkIds = /* @__PURE__ */ new Set();
19158
+ initialCalendarRepairIds = /* @__PURE__ */ new Set();
18542
19159
  linksAtLastSave = /* @__PURE__ */ new Map();
18543
19160
  activitiesAtLastSave = /* @__PURE__ */ new Map();
19161
+ constructor(trackingEnabled = true) {
19162
+ this.trackingEnabled = trackingEnabled;
19163
+ }
19164
+ fork() {
19165
+ const fork = new _SaveTracker(false);
19166
+ fork.initialized = this.initialized;
19167
+ for (const [linkId, link] of this.linksAtLastSave) {
19168
+ fork.linksAtLastSave.set(linkId, cloneDomainValue(link));
19169
+ }
19170
+ for (const [activityId, activity] of this.activitiesAtLastSave) {
19171
+ fork.activitiesAtLastSave.set(activityId, cloneDomainValue(activity));
19172
+ }
19173
+ return fork;
19174
+ }
18544
19175
  init(activities, links) {
19176
+ if (!this.trackingEnabled) return;
19177
+ assertValidPersistedIdentities(activities, links);
18545
19178
  this.snapshotActivities(activities);
18546
19179
  this.snapshotLinks(links);
19180
+ for (const activity of activities) {
19181
+ if (activity.assignedDefaultCalendar) {
19182
+ const activityId = String(activity.id);
19183
+ this.initialCalendarRepairIds.add(activityId);
19184
+ this.dirtyActivityIds.add(activityId);
19185
+ }
19186
+ }
18547
19187
  this.initialized = true;
18548
19188
  }
19189
+ acknowledge(activities, links) {
19190
+ if (!this.trackingEnabled) return;
19191
+ assertValidPersistedIdentities(activities, links);
19192
+ this.snapshotActivities(activities);
19193
+ this.snapshotLinks(links);
19194
+ }
18549
19195
  snapshotActivities(activities) {
19196
+ if (!this.trackingEnabled) return;
18550
19197
  this.activitiesAtLastSave.clear();
18551
19198
  this.dirtyActivityIds.clear();
19199
+ this.initialCalendarRepairIds.clear();
18552
19200
  for (const activity of activities) {
18553
19201
  const activityId = String(activity.id);
18554
- if (activity.proplannerId == null) {
19202
+ if (!hasPersistedIdentity(activity.proplannerId)) {
18555
19203
  this.dirtyActivityIds.add(activityId);
18556
19204
  continue;
18557
19205
  }
@@ -18571,11 +19219,12 @@ var SaveTracker = class {
18571
19219
  }
18572
19220
  }
18573
19221
  snapshotLinks(links) {
19222
+ if (!this.trackingEnabled) return;
18574
19223
  this.linksAtLastSave.clear();
18575
19224
  this.dirtyLinkIds.clear();
18576
19225
  for (const link of links) {
18577
19226
  const linkId = String(link.id);
18578
- if (link.proplannerId == null) {
19227
+ if (!hasPersistedIdentity(link.proplannerId)) {
18579
19228
  this.dirtyLinkIds.add(linkId);
18580
19229
  continue;
18581
19230
  }
@@ -18590,12 +19239,11 @@ var SaveTracker = class {
18590
19239
  }
18591
19240
  }
18592
19241
  recordChanges(activities, links, getActivity, getLink) {
18593
- if (!this.initialized) return;
19242
+ if (!this.trackingEnabled || !this.initialized) return;
18594
19243
  for (const change of activities) {
18595
19244
  const activityId = String(change.id);
18596
19245
  const current = getActivity(activityId);
18597
- const saved = this.activitiesAtLastSave.get(activityId);
18598
- if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || activityChangedSince(current, saved.persistable)) {
19246
+ if (this.activityIsDirty(activityId, current)) {
18599
19247
  this.dirtyActivityIds.add(activityId);
18600
19248
  } else {
18601
19249
  this.dirtyActivityIds.delete(activityId);
@@ -18604,81 +19252,147 @@ var SaveTracker = class {
18604
19252
  for (const change of links) {
18605
19253
  const linkId = String(change.id);
18606
19254
  const current = getLink(linkId);
18607
- const saved = this.linksAtLastSave.get(linkId);
18608
- if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || linkChangedSince(saved, current)) {
19255
+ if (this.linkIsDirty(linkId, current)) {
18609
19256
  this.dirtyLinkIds.add(linkId);
18610
19257
  } else {
18611
19258
  this.dirtyLinkIds.delete(linkId);
18612
19259
  }
18613
19260
  }
18614
19261
  }
18615
- acknowledgePersistedLookahead(activityIds, getActivity) {
18616
- if (!this.initialized) return;
18617
- for (const activityId of activityIds) {
19262
+ activityIsDirty(activityId, current) {
19263
+ const saved = this.activitiesAtLastSave.get(activityId);
19264
+ if (current === null) return saved !== void 0;
19265
+ return this.initialCalendarRepairIds.has(activityId) || !hasPersistedIdentity(current.proplannerId) || saved === void 0 || activityChangedSince(current, saved.persistable);
19266
+ }
19267
+ linkIsDirty(linkId, current) {
19268
+ const saved = this.linksAtLastSave.get(linkId);
19269
+ if (current === null) return saved !== void 0;
19270
+ return !hasPersistedIdentity(current.proplannerId) || saved === void 0 || linkChangedSince(saved, current);
19271
+ }
19272
+ acknowledgePersistedLookahead(changes, getActivity) {
19273
+ if (!this.trackingEnabled || !this.initialized) return;
19274
+ const acknowledgements = changes.map((change) => {
19275
+ const activityId = typeof change === "string" ? change : String(change.id);
18618
19276
  const current = getActivity(activityId);
18619
19277
  const saved = this.activitiesAtLastSave.get(activityId);
18620
- if (current === null || saved === void 0) continue;
19278
+ const isUnknownPersistedActivity = current === null || saved === void 0;
19279
+ if (isUnknownPersistedActivity) {
19280
+ throw new Error(`unknown persisted activity "${activityId}"`);
19281
+ }
19282
+ return {
19283
+ activityId,
19284
+ acknowledgesLookahead: typeof change === "string" || Boolean(change.fields?.isLookahead),
19285
+ acknowledgesProgress: typeof change !== "string" && Boolean(change.fields?.progress),
19286
+ current,
19287
+ saved
19288
+ };
19289
+ });
19290
+ for (const {
19291
+ activityId,
19292
+ acknowledgesLookahead,
19293
+ acknowledgesProgress,
19294
+ current,
19295
+ saved
19296
+ } of acknowledgements) {
18621
19297
  const persistable = {
18622
19298
  ...saved.persistable,
18623
- isLookahead: current.isLookahead
19299
+ ...acknowledgesLookahead ? { isLookahead: current.isLookahead } : {},
19300
+ ...acknowledgesProgress ? { progress: current.progress } : {}
18624
19301
  };
18625
19302
  this.activitiesAtLastSave.set(activityId, {
18626
19303
  ...saved,
18627
19304
  persistable
18628
19305
  });
18629
- if (current.proplannerId == null || activityChangedSince(current, persistable)) {
19306
+ if (this.initialCalendarRepairIds.has(activityId) || !hasPersistedIdentity(current.proplannerId) || activityChangedSince(current, persistable)) {
18630
19307
  this.dirtyActivityIds.add(activityId);
18631
19308
  } else {
18632
19309
  this.dirtyActivityIds.delete(activityId);
18633
19310
  }
18634
19311
  }
18635
19312
  }
19313
+ hasCheckpointActivityId(activityId) {
19314
+ return this.activitiesAtLastSave.has(activityId);
19315
+ }
19316
+ hasCheckpointLinkId(linkId) {
19317
+ return this.linksAtLastSave.has(linkId);
19318
+ }
18636
19319
  hasUnsavedChanges() {
18637
- return this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
19320
+ return this.trackingEnabled && this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
19321
+ }
19322
+ newActivities(getActivity) {
19323
+ if (!this.trackingEnabled || !this.initialized) return [];
19324
+ const activities = [];
19325
+ for (const activityId of this.dirtyActivityIds) {
19326
+ const activity = getActivity(activityId);
19327
+ const isNewActivity = activity !== null && !hasPersistedIdentity(activity.proplannerId);
19328
+ if (isNewActivity) activities.push(activity);
19329
+ }
19330
+ return activities;
19331
+ }
19332
+ newLinks(getLink) {
19333
+ if (!this.trackingEnabled || !this.initialized) return [];
19334
+ const links = [];
19335
+ for (const linkId of this.dirtyLinkIds) {
19336
+ const link = getLink(linkId);
19337
+ const isNewLink = link !== null && !hasPersistedIdentity(link.proplannerId);
19338
+ if (isNewLink) links.push(link);
19339
+ }
19340
+ return links;
19341
+ }
19342
+ deletedActivities(getActivity) {
19343
+ if (!this.trackingEnabled || !this.initialized) return [];
19344
+ const deleted = [];
19345
+ for (const activityId of this.dirtyActivityIds) {
19346
+ const saved = this.activitiesAtLastSave.get(activityId);
19347
+ if (saved && getActivity(activityId) === null) {
19348
+ deleted.push({ ...saved.deletedSnapshot });
19349
+ }
19350
+ }
19351
+ return deleted;
18638
19352
  }
18639
- deletedActivities(activities) {
18640
- if (!this.initialized) return [];
18641
- const currentIds = new Set(
18642
- activities.map((activity) => String(activity.id))
18643
- );
19353
+ deletedLinks(getLink) {
19354
+ if (!this.trackingEnabled || !this.initialized) return [];
18644
19355
  const deleted = [];
18645
- for (const [activityId, saved] of this.activitiesAtLastSave) {
18646
- if (currentIds.has(activityId)) continue;
18647
- deleted.push({ ...saved.deletedSnapshot });
19356
+ for (const linkId of this.dirtyLinkIds) {
19357
+ const saved = this.linksAtLastSave.get(linkId);
19358
+ if (saved && getLink(linkId) === null) deleted.push({ ...saved });
18648
19359
  }
18649
19360
  return deleted;
18650
19361
  }
18651
- deletedLinks(links) {
18652
- if (!this.initialized) return [];
18653
- const currentIds = new Set(links.map((link) => String(link.id)));
18654
- return Array.from(
18655
- this.linksAtLastSave.entries(),
18656
- ([linkId, saved]) => currentIds.has(linkId) ? null : { ...saved }
18657
- ).filter((link) => link !== null);
18658
- }
18659
- modifiedActivities(activities) {
18660
- if (!this.initialized) return [];
18661
- return activities.filter((activity) => {
18662
- if (activity.proplannerId == null) return false;
18663
- if (activity.assignedDefaultCalendar) return true;
18664
- const saved = this.activitiesAtLastSave.get(String(activity.id));
18665
- if (!saved) return true;
18666
- return activityChangedSince(activity, saved.persistable);
18667
- });
19362
+ modifiedActivities(getActivity) {
19363
+ if (!this.trackingEnabled || !this.initialized) return [];
19364
+ const modified = [];
19365
+ for (const activityId of this.dirtyActivityIds) {
19366
+ const activity = getActivity(activityId);
19367
+ if (activity === null || !hasPersistedIdentity(activity.proplannerId)) {
19368
+ continue;
19369
+ }
19370
+ const saved = this.activitiesAtLastSave.get(activityId);
19371
+ if (saved === void 0 || this.initialCalendarRepairIds.has(activityId) || activityChangedSince(activity, saved.persistable)) {
19372
+ modified.push(activity);
19373
+ }
19374
+ }
19375
+ return modified;
18668
19376
  }
18669
- modifiedLinks(links) {
18670
- if (!this.initialized) return [];
18671
- return links.filter((link) => {
18672
- if (link.proplannerId == null) return false;
18673
- const saved = this.linksAtLastSave.get(String(link.id));
18674
- if (!saved) return true;
18675
- return linkChangedSince(saved, link);
18676
- });
19377
+ modifiedLinks(getLink) {
19378
+ if (!this.trackingEnabled || !this.initialized) return [];
19379
+ const modified = [];
19380
+ for (const linkId of this.dirtyLinkIds) {
19381
+ const link = getLink(linkId);
19382
+ if (link === null) continue;
19383
+ if (!hasPersistedIdentity(link.proplannerId)) continue;
19384
+ const saved = this.linksAtLastSave.get(linkId);
19385
+ if (saved === void 0 || linkChangedSince(saved, link)) {
19386
+ modified.push(link);
19387
+ }
19388
+ }
19389
+ return modified;
18677
19390
  }
18678
19391
  clear() {
18679
19392
  this.initialized = false;
18680
19393
  this.dirtyActivityIds.clear();
18681
19394
  this.dirtyLinkIds.clear();
19395
+ this.initialCalendarRepairIds.clear();
18682
19396
  this.linksAtLastSave.clear();
18683
19397
  this.activitiesAtLastSave.clear();
18684
19398
  }
@@ -18732,14 +19446,14 @@ function isAutoSchedulable(progress) {
18732
19446
  }
18733
19447
  function parseConstraintLabel(label, reporter) {
18734
19448
  if (!label) return null;
18735
- const normalized = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
18736
- if (normalized === void 0) {
19449
+ const normalized2 = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
19450
+ if (normalized2 === void 0) {
18737
19451
  reporter.warn(
18738
19452
  `[parseFromBackend] unknown constraint label "${label}" \u2014 defaulting to "asap"`
18739
19453
  );
18740
19454
  return DEFAULT_CONSTRAINT_TYPE;
18741
19455
  }
18742
- return normalized;
19456
+ return normalized2;
18743
19457
  }
18744
19458
  function resolveConstraint(raw, reporter) {
18745
19459
  const constraintType = parseConstraintLabel(raw.constraint, reporter);
@@ -18780,6 +19494,24 @@ function parseIdList(raw) {
18780
19494
  function parseParentId(rawParentId) {
18781
19495
  return rawParentId === ROOT_PARENT_ID ? null : String(rawParentId);
18782
19496
  }
19497
+ function normalizePersistedActivityId(rawId) {
19498
+ if (rawId == null) return null;
19499
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19500
+ if (!isValidIdentity) {
19501
+ throw new Error(`invalid activity proplannerId ${String(rawId)}`);
19502
+ }
19503
+ return rawId;
19504
+ }
19505
+ function parseCriticality(raw, reporter) {
19506
+ if (typeof raw.is_critical === "boolean") return raw.is_critical;
19507
+ reporter.error(
19508
+ "[parseFromBackend] invalid activity is_critical \u2014 defaulting to false",
19509
+ new TypeError(
19510
+ `activity "${raw.unique_id}" received ${typeof raw.is_critical}`
19511
+ )
19512
+ );
19513
+ return false;
19514
+ }
18783
19515
  function parseBaselinePoints(raw, reporter) {
18784
19516
  if (!Array.isArray(raw)) return [];
18785
19517
  return raw.map((point) => normalizeBaselinePoint(point, reporter)).filter((point) => point !== null);
@@ -18800,9 +19532,10 @@ function parseActivity(raw, context) {
18800
19532
  const startDate = parseBackendDate(raw.start_date);
18801
19533
  const endDate = isMilestone ? startDate : parseBackendDate(raw.end_date);
18802
19534
  const durationDays = isMilestone ? 0 : Number(raw.duration);
19535
+ const isCritical = parseCriticality(raw, context.reporter);
18803
19536
  return {
18804
19537
  id: String(raw.unique_id),
18805
- proplannerId: typeof raw.id === "number" ? raw.id : null,
19538
+ proplannerId: normalizePersistedActivityId(raw.id),
18806
19539
  parentId: parseParentId(raw.parent_id),
18807
19540
  name: raw.name,
18808
19541
  description: raw.description ?? "",
@@ -18847,7 +19580,7 @@ function parseActivity(raw, context) {
18847
19580
  startDate,
18848
19581
  endDate
18849
19582
  }),
18850
- isCritical: Boolean(raw.is_critical),
19583
+ isCritical,
18851
19584
  freeSlackHours: null,
18852
19585
  expectedProgress: null,
18853
19586
  expectedProgressBaseline: null,
@@ -19104,6 +19837,14 @@ function resolveLinkType(rawType) {
19104
19837
  if (isLinkTypeCode(rawType)) return rawType;
19105
19838
  return null;
19106
19839
  }
19840
+ function normalizePersistedLinkId(rawId) {
19841
+ if (rawId == null) return void 0;
19842
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19843
+ if (!isValidIdentity) {
19844
+ throw new Error(`invalid link proplannerId ${String(rawId)}`);
19845
+ }
19846
+ return rawId;
19847
+ }
19107
19848
  function normalizeLinks(rawLinks, context) {
19108
19849
  const parsedLinks = [];
19109
19850
  for (const rawLink of rawLinks) {
@@ -19142,16 +19883,18 @@ function parseLink(link, activityIds, hoursPerDay) {
19142
19883
  }
19143
19884
  const ganttIdRaw = link.ganttId;
19144
19885
  const sectorIdRaw = link.sectorId;
19145
- const proplannerIdRaw = link.id;
19886
+ const proplannerId = normalizePersistedLinkId(link.id);
19146
19887
  return {
19147
19888
  id,
19148
19889
  source,
19149
19890
  target,
19150
19891
  type,
19151
19892
  lag: lagDaysToHours(Number(link.lag ?? 0), hoursPerDay),
19893
+ freeSlackHours: null,
19894
+ isCritical: false,
19152
19895
  ...typeof ganttIdRaw === "number" ? { ganttId: ganttIdRaw } : {},
19153
19896
  ...typeof sectorIdRaw === "number" ? { sectorId: sectorIdRaw } : {},
19154
- ...typeof proplannerIdRaw === "number" ? { proplannerId: proplannerIdRaw } : {}
19897
+ ...proplannerId === void 0 ? {} : { proplannerId }
19155
19898
  };
19156
19899
  }
19157
19900
  function dedupeLinksBySignature(links) {
@@ -19278,6 +20021,9 @@ function createActivityIdGenerator(options = {}) {
19278
20021
  }
19279
20022
  }
19280
20023
  return {
20024
+ fork() {
20025
+ return createActivityIdGenerator({ seed: counter });
20026
+ },
19281
20027
  next() {
19282
20028
  counter += 1;
19283
20029
  return String(counter);
@@ -19309,6 +20055,9 @@ function createLinkIdGenerator(options = {}) {
19309
20055
  }
19310
20056
  }
19311
20057
  return {
20058
+ fork() {
20059
+ return createLinkIdGenerator({ seed: counter });
20060
+ },
19312
20061
  next() {
19313
20062
  counter += 1;
19314
20063
  return String(counter);
@@ -19344,6 +20093,9 @@ function createUniqueCorrelativeIdGenerator(options = {}) {
19344
20093
  }
19345
20094
  }
19346
20095
  return {
20096
+ fork() {
20097
+ return createUniqueCorrelativeIdGenerator({ seed: counter });
20098
+ },
19347
20099
  next() {
19348
20100
  counter += 1;
19349
20101
  return String(counter);
@@ -19471,7 +20223,7 @@ function selectGenerationStrategy(customId, useActivityCustomIdAsReference) {
19471
20223
  function cloneStats(stats) {
19472
20224
  return { ...stats, usedSuffixes: new Set(stats.usedSuffixes) };
19473
20225
  }
19474
- var CustomIdTracker = class {
20226
+ var CustomIdTracker = class _CustomIdTracker {
19475
20227
  prefixMap = /* @__PURE__ */ new Map();
19476
20228
  processedIds = /* @__PURE__ */ new Set();
19477
20229
  _journalPrefixBefore = null;
@@ -19532,6 +20284,19 @@ var CustomIdTracker = class {
19532
20284
  this.defaultPrefix = options.defaultPrefix ?? DEFAULT_PREFIX;
19533
20285
  this.suffixIncrement = options.suffixIncrement ?? DEFAULT_SUFFIX_INCREMENT;
19534
20286
  }
20287
+ fork() {
20288
+ const fork = new _CustomIdTracker({
20289
+ defaultPrefix: this.defaultPrefix,
20290
+ suffixIncrement: this.suffixIncrement
20291
+ });
20292
+ for (const [prefix, stats] of this.prefixMap) {
20293
+ fork.prefixMap.set(prefix, cloneStats(stats));
20294
+ }
20295
+ for (const customId of this.processedIds) {
20296
+ fork.processedIds.add(customId);
20297
+ }
20298
+ return fork;
20299
+ }
19535
20300
  extractPrefix(customId) {
19536
20301
  return extractPrefix(customId);
19537
20302
  }
@@ -20138,6 +20903,133 @@ function initializeCore(input) {
20138
20903
  };
20139
20904
  }
20140
20905
 
20906
+ // src/previsualization/canonical-digest.ts
20907
+ function normalized(value) {
20908
+ if (value === void 0) return ["undefined"];
20909
+ if (value === null) return ["null"];
20910
+ if (typeof value === "string") return ["string", value];
20911
+ if (typeof value === "boolean") return ["boolean", value];
20912
+ if (typeof value === "number") {
20913
+ return ["number", Object.is(value, -0) ? "-0" : String(value)];
20914
+ }
20915
+ if (typeof value === "bigint") return ["bigint", String(value)];
20916
+ if (value instanceof Date) return ["date", String(value.getTime())];
20917
+ if (Array.isArray(value)) return ["array", value.map(normalized)];
20918
+ if (value instanceof Map) {
20919
+ return [
20920
+ "map",
20921
+ Array.from(value, ([key, entry]) => [
20922
+ normalized(key),
20923
+ normalized(entry)
20924
+ ]).sort(
20925
+ (left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))
20926
+ )
20927
+ ];
20928
+ }
20929
+ if (value instanceof Set) {
20930
+ return [
20931
+ "set",
20932
+ Array.from(value, normalized).sort(
20933
+ (left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))
20934
+ )
20935
+ ];
20936
+ }
20937
+ if (typeof value === "object") {
20938
+ return [
20939
+ "object",
20940
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalized(entry)])
20941
+ ];
20942
+ }
20943
+ throw new TypeError(`Unsupported canonical value: ${typeof value}`);
20944
+ }
20945
+ function serializeCanonicalSchedule(value) {
20946
+ return JSON.stringify(normalized(value));
20947
+ }
20948
+
20949
+ // src/previsualization/reconciliation.ts
20950
+ function entityChanges(target, source) {
20951
+ const targetById = new Map(
20952
+ target.map((entity) => [String(entity.id), entity])
20953
+ );
20954
+ const sourceById = new Map(
20955
+ source.map((entity) => [String(entity.id), entity])
20956
+ );
20957
+ const ids = [.../* @__PURE__ */ new Set([...targetById.keys(), ...sourceById.keys()])].sort();
20958
+ const changes = [];
20959
+ for (const id of ids) {
20960
+ const before = sourceById.get(id);
20961
+ const after = targetById.get(id);
20962
+ if (after === void 0) {
20963
+ changes.push({ id, kind: "deleted", after: null });
20964
+ continue;
20965
+ }
20966
+ if (before === void 0) {
20967
+ changes.push({ id, kind: "created", after: cloneDomainValue(after) });
20968
+ continue;
20969
+ }
20970
+ const fields = changedFields(before, after);
20971
+ if (Object.keys(fields).length > 0) {
20972
+ changes.push({
20973
+ id,
20974
+ kind: "updated",
20975
+ fields,
20976
+ after: cloneDomainValue(after)
20977
+ });
20978
+ }
20979
+ }
20980
+ return changes;
20981
+ }
20982
+ function changedFields(before, after) {
20983
+ const fields = {};
20984
+ const keys = [
20985
+ .../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])
20986
+ ].sort();
20987
+ for (const key of keys) {
20988
+ if (serializeCanonicalSchedule(before[key]) !== serializeCanonicalSchedule(after[key])) {
20989
+ fields[key] = {
20990
+ before: cloneDomainValue(before[key]),
20991
+ after: cloneDomainValue(after[key])
20992
+ };
20993
+ }
20994
+ }
20995
+ return fields;
20996
+ }
20997
+ function viewStateChanges(target, source) {
20998
+ const selectedBefore = new Set(source.selectedIds.map(String));
20999
+ const selectedAfter = new Set(target.selectedIds.map(String));
21000
+ const hiddenBefore = new Set(source.hiddenIds.map(String));
21001
+ const hiddenAfter = new Set(target.hiddenIds.map(String));
21002
+ const targetIds = target.activities.map(({ id }) => String(id)).sort();
21003
+ const changes = [];
21004
+ for (const activityId of targetIds) {
21005
+ const checkedBefore = selectedBefore.has(activityId);
21006
+ const checkedAfter = selectedAfter.has(activityId);
21007
+ const visibleBefore = !hiddenBefore.has(activityId);
21008
+ const visibleAfter = !hiddenAfter.has(activityId);
21009
+ if (checkedBefore === checkedAfter && visibleBefore === visibleAfter)
21010
+ continue;
21011
+ changes.push({
21012
+ activityId,
21013
+ ...checkedBefore === checkedAfter ? {} : { checked: { before: checkedBefore, after: checkedAfter } },
21014
+ ...visibleBefore === visibleAfter ? {} : { visible: { before: visibleBefore, after: visibleAfter } }
21015
+ });
21016
+ }
21017
+ return changes;
21018
+ }
21019
+ function createProjectionChangeSet(target, source) {
21020
+ const orderChanged = serializeCanonicalSchedule(target.order) !== serializeCanonicalSchedule(source.order);
21021
+ return {
21022
+ source: { kind: "init" },
21023
+ activities: entityChanges(target.activities, source.activities),
21024
+ links: entityChanges(target.links, source.links),
21025
+ calendars: entityChanges(target.calendars, source.calendars),
21026
+ trackingEvents: [],
21027
+ effects: [],
21028
+ viewState: viewStateChanges(target, source),
21029
+ ...orderChanged ? { order: cloneDomainValue(target.order) } : {}
21030
+ };
21031
+ }
21032
+
20141
21033
  // src/propagations/upward/date-gesture-preview.ts
20142
21034
  var emptyFormula = () => ({
20143
21035
  constantStart: null,
@@ -20283,7 +21175,10 @@ function evaluateEnd(formula, proposedEdgeTimeMs) {
20283
21175
  }
20284
21176
 
20285
21177
  // src/init/schedule-core.ts
20286
- var PREVIEW_DISPATCH_KINDS = /* @__PURE__ */ new Set(["inline-edit", "bulk-edit", "dates-batch"]);
21178
+ var FORK_SOURCE = /* @__PURE__ */ Symbol("ScheduleCore.fork.source");
21179
+ function acknowledgesPersistence(action) {
21180
+ return action.kind === "persistence-acknowledge" || action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true;
21181
+ }
20287
21182
  function changesRequireFilterProjectionRebuild(changes) {
20288
21183
  for (const change of changes.activities) {
20289
21184
  if (change.kind === "deleted") return true;
@@ -20301,16 +21196,46 @@ function collectChangedActivities(changes, adapter) {
20301
21196
  }
20302
21197
  return activities;
20303
21198
  }
21199
+ function changesAffectRelativePinProjection(changes, changedActivities, adapter, pins) {
21200
+ if (pins.size === 0) return false;
21201
+ if (changes.activities.some(
21202
+ (change) => change.kind === "created" && pins.has(String(change.id))
21203
+ )) {
21204
+ return true;
21205
+ }
21206
+ const referenceIds = new Set(
21207
+ [...pins.values()].map(({ referenceId }) => String(referenceId))
21208
+ );
21209
+ for (const activity of changedActivities) {
21210
+ let activityId = String(activity.id);
21211
+ while (activityId !== null) {
21212
+ if (referenceIds.has(activityId)) return true;
21213
+ activityId = adapter.getParentId(activityId);
21214
+ }
21215
+ }
21216
+ return false;
21217
+ }
20304
21218
  function isCurrentFilterProjection(projection, filter, context) {
20305
21219
  if (projection === null || projection.filter !== filter) return false;
20306
21220
  const hasCurrentHours = projection.context.hoursPerDay === context.hoursPerDay;
20307
21221
  const hasCurrentLocale = projection.context.locale === context.locale;
20308
21222
  return hasCurrentHours && hasCurrentLocale;
20309
21223
  }
20310
- var ScheduleCore = class {
21224
+ function relativePlacementPinsEqual(left, right) {
21225
+ if (left.size !== right.size) return false;
21226
+ for (const [activityId, pin] of left) {
21227
+ const other = right.get(activityId);
21228
+ if (other === void 0 || other.referenceId !== pin.referenceId || other.branchParentId !== pin.branchParentId || other.side !== pin.side) {
21229
+ return false;
21230
+ }
21231
+ }
21232
+ return true;
21233
+ }
21234
+ var ScheduleCore = class _ScheduleCore {
20311
21235
  _activityClipboard = { value: null };
20312
21236
  _status = SCHEDULE_CORE_STATUS.READY;
20313
21237
  coreRuntime;
21238
+ _isFork;
20314
21239
  _undo = new UndoRecorder();
20315
21240
  _opQueue = Promise.resolve();
20316
21241
  _scheduleRevision = 0;
@@ -20341,6 +21266,42 @@ var ScheduleCore = class {
20341
21266
  _criticalPathRevision = -1;
20342
21267
  _activeCriticalPath = null;
20343
21268
  constructor(input) {
21269
+ if (FORK_SOURCE in input) {
21270
+ this._isFork = true;
21271
+ const source = input[FORK_SOURCE];
21272
+ const state = source.coreRuntime.state.fork();
21273
+ this.coreRuntime = {
21274
+ state,
21275
+ scheduler: new AutoScheduler(state, source.coreRuntime.reporter),
21276
+ sector: cloneDomainValue(source.coreRuntime.sector),
21277
+ activityIdGenerator: source.coreRuntime.activityIdGenerator.fork(),
21278
+ linkIdGenerator: source.coreRuntime.linkIdGenerator.fork(),
21279
+ uniqueCorrelativeIdGenerator: source.coreRuntime.uniqueCorrelativeIdGenerator.fork(),
21280
+ customIdTracker: source.coreRuntime.customIdTracker.fork(),
21281
+ calendars: new Map(
21282
+ Array.from(source.coreRuntime.calendars, ([id, calendar]) => [
21283
+ id,
21284
+ cloneDomainValue(calendar)
21285
+ ])
21286
+ ),
21287
+ baseCalendars: cloneDomainValue(source.coreRuntime.baseCalendars),
21288
+ clock: source.coreRuntime.clock,
21289
+ reporter: source.coreRuntime.reporter
21290
+ };
21291
+ this._activityClipboard.value = cloneDomainValue(
21292
+ source._activityClipboard.value
21293
+ );
21294
+ this._undo = source._undo.fork();
21295
+ this._saveTracker = source._saveTracker.fork();
21296
+ this._scheduleRevision = source._scheduleRevision;
21297
+ this._pendingScheduleRevision = source._scheduleRevision;
21298
+ this._criticalPathRevision = source._criticalPathRevision;
21299
+ this._initPromise = Promise.resolve();
21300
+ this._criticalPathReady = Promise.resolve(null);
21301
+ this._ready = this._initPromise;
21302
+ return;
21303
+ }
21304
+ this._isFork = false;
20344
21305
  const initialized = initializeCore(input);
20345
21306
  this.coreRuntime = {
20346
21307
  state: initialized.state,
@@ -20377,6 +21338,15 @@ var ScheduleCore = class {
20377
21338
  get ready() {
20378
21339
  return this._ready;
20379
21340
  }
21341
+ fork() {
21342
+ return this._enqueue(async () => {
21343
+ await this._ready;
21344
+ this.assertReady();
21345
+ return new _ScheduleCore({
21346
+ [FORK_SOURCE]: this
21347
+ });
21348
+ });
21349
+ }
20380
21350
  get criticalPathReady() {
20381
21351
  return this._criticalPathReady;
20382
21352
  }
@@ -20429,6 +21399,21 @@ var ScheduleCore = class {
20429
21399
  this.assertReady();
20430
21400
  return readLink(this.coreRuntime.state, id);
20431
21401
  }
21402
+ getLinksForActivityView(activityId) {
21403
+ this.assertReady();
21404
+ const state = this.coreRuntime.state;
21405
+ const links = [];
21406
+ const seen = /* @__PURE__ */ new Set();
21407
+ const append = (linkId) => {
21408
+ if (seen.has(linkId)) return;
21409
+ seen.add(linkId);
21410
+ const link = readLink(state, linkId);
21411
+ if (link) links.push(link);
21412
+ };
21413
+ for (const linkId of state.getOutgoingLinkIds(activityId)) append(linkId);
21414
+ for (const linkId of state.getIncomingLinkIds(activityId)) append(linkId);
21415
+ return links;
21416
+ }
20432
21417
  getAllLinksView() {
20433
21418
  this.assertReady();
20434
21419
  return readAllLinks(this.coreRuntime.state);
@@ -20526,91 +21511,65 @@ var ScheduleCore = class {
20526
21511
  }
20527
21512
  getModifiedLinks() {
20528
21513
  this.assertReady();
20529
- return this._saveTracker.modifiedLinks(this.getAllLinksView());
21514
+ return this._saveTracker.modifiedLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
21515
+ }
21516
+ getNewLinks() {
21517
+ this.assertReady();
21518
+ return this._saveTracker.newLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
20530
21519
  }
20531
21520
  getModifiedActivities() {
20532
21521
  this.assertReady();
20533
- return this._saveTracker.modifiedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
21522
+ return this._saveTracker.modifiedActivities(
21523
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21524
+ ).map(cloneDomainValue);
21525
+ }
21526
+ getNewActivities() {
21527
+ this.assertReady();
21528
+ return this._saveTracker.newActivities(
21529
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21530
+ ).map(cloneDomainValue);
20534
21531
  }
20535
21532
  hasUnsavedChanges() {
20536
21533
  this.assertReady();
20537
21534
  return this._saveTracker.hasUnsavedChanges();
20538
21535
  }
20539
- /**
20540
- * Read-only view of the internal schedule revision. It moves once per
20541
- * substantive CANONICAL mutation (dispatch, undo, redo). It does NOT move
20542
- * for a preview dispatch nor for `applyPreviewInverse`: comparing the value
20543
- * captured in `PreviewDispatchInfo.revision` against the current one
20544
- * answers "did the schedule change while the preview was open".
20545
- */
21536
+ /** Read-only revision; moves once per substantive mutation. */
20546
21537
  getScheduleRevision() {
20547
21538
  return this._scheduleRevision;
20548
21539
  }
20549
- /**
20550
- * Exit channel for a preview dispatch (`DispatchOptions.preview`): applies
20551
- * the inverse ChangeSet the preview returned, restoring the canonical
20552
- * state exactly. Like the preview itself it leaves no session trace: no
20553
- * save-tracker dirty, no undo entry, no effects, and the schedule revision
20554
- * does not move (see `getScheduleRevision`).
20555
- *
20556
- * Returns the ChangeSet to project to the view layer: the same changes as
20557
- * `inverse`, but with `after` images re-read from the restored state (the
20558
- * ones inside `inverse` were captured while the preview edit was applied).
20559
- */
20560
- applyPreviewInverse(inverse) {
20561
- return this._enqueue(async () => {
20562
- this.assertReady();
20563
- const state = this.coreRuntime.state;
20564
- const afterSnap = /* @__PURE__ */ new Map();
20565
- for (const change of inverse.activities) {
20566
- if (change.kind !== "deleted" && change.after) {
20567
- afterSnap.set(
20568
- String(change.id),
20569
- structuredCloneActivity(change.after)
20570
- );
20571
- }
20572
- }
20573
- const afterLinks = /* @__PURE__ */ new Map();
20574
- for (const change of inverse.links) {
20575
- if (change.kind !== "deleted" && change.after) {
20576
- afterLinks.set(String(change.id), { ...change.after });
20577
- }
20578
- }
20579
- const entry = buildUndoEntry(
20580
- inverse,
20581
- void 0,
20582
- void 0,
20583
- afterSnap,
20584
- afterLinks
20585
- );
20586
- applyRedo(state, entry);
20587
- if (needsResync(entry)) this._resyncCustomIdTrackerFromModel();
20588
- const restored = {
20589
- ...inverse,
20590
- activities: inverse.activities.map((change) => {
20591
- if (change.kind === "deleted") return { ...change, after: null };
20592
- const live = state.getActivity(String(change.id));
20593
- return {
20594
- ...change,
20595
- after: live ? structuredCloneActivity(live) : null
20596
- };
20597
- }),
20598
- links: inverse.links.map((change) => {
20599
- if (change.kind === "deleted") return { ...change, after: null };
20600
- const live = state.getLink(String(change.id));
20601
- return { ...change, after: live ? snapshotToLink(live) : null };
20602
- })
20603
- };
20604
- return this._reapplyViewState(restored) ?? restored;
21540
+ getCanonicalDigest() {
21541
+ this.assertReady();
21542
+ const byId = (left, right) => String(left.id).localeCompare(String(right.id));
21543
+ return serializeCanonicalSchedule({
21544
+ sector: this.getSector(),
21545
+ activities: [...this.getAllActivitiesView()].sort(byId),
21546
+ links: [...this.getAllLinksView()].sort(byId),
21547
+ calendars: [...this.getAllCalendars()].sort(byId),
21548
+ baseCalendars: [...this.getBaseCalendars()].sort(byId)
21549
+ });
21550
+ }
21551
+ createProjectionChangeSetFrom(source) {
21552
+ this.assertReady();
21553
+ source.assertReady();
21554
+ const projectionOf = (core) => ({
21555
+ activities: core.getAllActivitiesView(),
21556
+ links: core.getAllLinksView(),
21557
+ calendars: core.getAllCalendars(),
21558
+ selectedIds: core.getSelectedActivityIds(),
21559
+ hiddenIds: core.getHiddenActivityIds(),
21560
+ order: collectBranchOrder(core.coreRuntime.state)
20605
21561
  });
21562
+ return createProjectionChangeSet(projectionOf(this), projectionOf(source));
20606
21563
  }
20607
21564
  getDeletedActivitiesSinceLastSave() {
20608
21565
  this.assertReady();
20609
- return this._saveTracker.deletedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
21566
+ return this._saveTracker.deletedActivities(
21567
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21568
+ ).map(cloneDomainValue);
20610
21569
  }
20611
21570
  getDeletedLinksSinceLastSave() {
20612
21571
  this.assertReady();
20613
- return this._saveTracker.deletedLinks(this.getAllLinksView()).map(cloneDomainValue);
21572
+ return this._saveTracker.deletedLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
20614
21573
  }
20615
21574
  _enqueue(work) {
20616
21575
  const workPromise = this._opQueue.then(work, work);
@@ -20686,11 +21645,11 @@ var ScheduleCore = class {
20686
21645
  });
20687
21646
  }
20688
21647
  async dispatch(action, options = {}) {
20689
- const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
21648
+ const admitted = this._beginScheduleMutation(action);
20690
21649
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
20691
21650
  }
20692
21651
  async applyActivityBatch(action, options = {}) {
20693
- const admitted = options.preview === true ? false : this._beginScheduleMutation(action);
21652
+ const admitted = this._beginScheduleMutation(action);
20694
21653
  return this._enqueue(() => this._dispatchInner(action, options, admitted));
20695
21654
  }
20696
21655
  async _dispatchInner(action, options = {}, admitted = false) {
@@ -20705,23 +21664,22 @@ var ScheduleCore = class {
20705
21664
  }
20706
21665
  async _runDispatch(action, options) {
20707
21666
  this.assertReady();
20708
- if (options.preview === true && !PREVIEW_DISPATCH_KINDS.has(action.kind)) {
21667
+ if (this._isFork && acknowledgesPersistence(action)) {
20709
21668
  return {
20710
21669
  result: {
20711
21670
  ok: false,
20712
- reason: REJECTION_REASON.PREVIEW_UNSUPPORTED_ACTION
21671
+ reason: REJECTION_REASON.SANDBOX_PERSISTENCE_FORBIDDEN
20713
21672
  },
20714
21673
  changedState: false
20715
21674
  };
20716
21675
  }
20717
21676
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
20718
21677
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
20719
- dateFormat: this.coreRuntime.sector.dateFormat,
20720
- // ISSUE-057 (CP6): sin default silencioso 'days'; la clave se omite si
20721
- // no se declaró (exactOptionalPropertyTypes).
20722
- ...options.inputUnit !== void 0 ? { inputUnit: options.inputUnit } : {}
21678
+ dateFormat: this.coreRuntime.sector.dateFormat
20723
21679
  });
20724
- const historyPolicy = getDispatchHistoryPolicy(action, options);
21680
+ const historyPolicy = getDispatchHistoryPolicy(action);
21681
+ const capturesRelativePlacementPins = action.kind === "activity-create" || action.kind === "activity-delete" || action.kind === "activity-batch" && action.mode === "replace";
21682
+ const relativePlacementPinsBefore = capturesRelativePlacementPins ? new Map(this.coreRuntime.state.getRelativePlacementPins()) : void 0;
20725
21683
  const generatorSnapshot = [
20726
21684
  this.coreRuntime.activityIdGenerator.snapshot(),
20727
21685
  this.coreRuntime.linkIdGenerator.snapshot(),
@@ -20739,6 +21697,8 @@ var ScheduleCore = class {
20739
21697
  calendars: this.coreRuntime.calendars,
20740
21698
  activityIdGen: this.coreRuntime.activityIdGenerator,
20741
21699
  linkIdGen: this.coreRuntime.linkIdGenerator,
21700
+ isActivityIdReserved: (activityId) => this._saveTracker.hasCheckpointActivityId(activityId),
21701
+ isLinkIdReserved: (linkId) => this._saveTracker.hasCheckpointLinkId(linkId),
20742
21702
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
20743
21703
  customIdTracker: this.coreRuntime.customIdTracker,
20744
21704
  reporter: this.coreRuntime.reporter,
@@ -20756,18 +21716,26 @@ var ScheduleCore = class {
20756
21716
  this.coreRuntime.state.endWriteCapture();
20757
21717
  this.coreRuntime.customIdTracker.commitCustomIdTransaction();
20758
21718
  }
21719
+ if (result.ok) this._recordRelativePlacementPin(action, result.changes);
20759
21720
  if (result.ok && historyPolicy === "record" && changeSetIsSubstantive(result.changes)) {
20760
21721
  const created = captureCreatedSnapshots(
20761
21722
  this.coreRuntime.state,
20762
21723
  result.changes
20763
21724
  );
21725
+ const relativePlacementPinsAfter = capturesRelativePlacementPins ? new Map(this.coreRuntime.state.getRelativePlacementPins()) : void 0;
21726
+ const relativePlacementPinsChanged = relativePlacementPinsBefore !== void 0 && relativePlacementPinsAfter !== void 0 && !relativePlacementPinsEqual(
21727
+ relativePlacementPinsBefore,
21728
+ relativePlacementPinsAfter
21729
+ );
20764
21730
  this._undo.record(
20765
21731
  buildUndoEntry(
20766
21732
  buildHistoryChangeSet(result.changes),
20767
21733
  result.__beforeSnap,
20768
21734
  result.__beforeLinks,
20769
21735
  created.afterSnap,
20770
- created.afterLinks
21736
+ created.afterLinks,
21737
+ relativePlacementPinsChanged ? relativePlacementPinsBefore : void 0,
21738
+ relativePlacementPinsChanged ? relativePlacementPinsAfter : void 0
20771
21739
  ),
20772
21740
  computeCoalesceKey(action),
20773
21741
  Date.now()
@@ -20775,17 +21743,20 @@ var ScheduleCore = class {
20775
21743
  }
20776
21744
  if (result.ok) {
20777
21745
  if (historyPolicy === "clear-on-success") {
20778
- this._saveTracker.snapshotActivities(
20779
- this.coreRuntime.state.getAllActivities()
21746
+ this._saveTracker.acknowledge(
21747
+ this.coreRuntime.state.getAllActivities(),
21748
+ this.coreRuntime.state.getAllLinks()
20780
21749
  );
20781
- this._saveTracker.snapshotLinks(this.getAllLinksView());
20782
21750
  this._undo.clear();
20783
21751
  } else if (action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true) {
20784
21752
  this._saveTracker.acknowledgePersistedLookahead(
20785
- action.activityIds,
21753
+ result.changes.activities,
20786
21754
  (activityId) => this.coreRuntime.state.getActivity(activityId)
20787
21755
  );
20788
- } else if (options.preview !== true) {
21756
+ if (action.persistedProgress !== void 0) {
21757
+ this._undo.clear();
21758
+ }
21759
+ } else {
20789
21760
  this._saveTracker.recordChanges(
20790
21761
  result.changes.activities,
20791
21762
  result.changes.links,
@@ -20796,20 +21767,6 @@ var ScheduleCore = class {
20796
21767
  }
20797
21768
  result = this._withReappliedViewState(action, result);
20798
21769
  if (!result.ok) return { result, changedState: false };
20799
- if (options.preview === true) {
20800
- const inverse = buildInverseChangeSet(
20801
- this.coreRuntime.state,
20802
- buildUndoEntry(buildHistoryChangeSet(result.changes)),
20803
- "before"
20804
- );
20805
- return {
20806
- result: {
20807
- ...toPublicDispatchResult(result),
20808
- preview: { inverse, revision: this._scheduleRevision }
20809
- },
20810
- changedState: false
20811
- };
20812
- }
20813
21770
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
20814
21771
  this._recordScheduleMutation(
20815
21772
  options.skipAutoSchedule !== true && options.skipCriticalPath !== true && resolveRunCriticalPath(action)
@@ -20846,6 +21803,19 @@ var ScheduleCore = class {
20846
21803
  const merged = this._reapplyViewState(result.changes);
20847
21804
  return merged === null ? result : { ...result, changes: merged };
20848
21805
  }
21806
+ _recordRelativePlacementPin(action, changes) {
21807
+ const state = this.coreRuntime.state;
21808
+ if (action.kind !== "activity-create") return;
21809
+ const referenceId = action.afterSiblingId ?? action.beforeSiblingId ?? (action.eventSource === "Creation button (+)" && action.parentId !== ROOT_PARENT_ID ? action.parentId : void 0);
21810
+ if (referenceId === void 0) return;
21811
+ const created = changes.activities.find(({ kind }) => kind === "created");
21812
+ if (created === void 0) return;
21813
+ state.setRelativePlacementPin(created.id, {
21814
+ referenceId,
21815
+ branchParentId: action.parentId,
21816
+ side: action.beforeSiblingId === void 0 ? "after" : "before"
21817
+ });
21818
+ }
20849
21819
  /**
20850
21820
  * The single place the two view-state passes are chained. Dispatch, undo and
20851
21821
  * redo all route through here: when this existed only inside the dispatch
@@ -20868,39 +21838,7 @@ var ScheduleCore = class {
20868
21838
  sequenced.changes ?? withFilter ?? changes,
20869
21839
  sequenced.touchedParents
20870
21840
  );
20871
- const reapplied = withOrder ?? sequenced.changes ?? withFilter;
20872
- const finalChanges = reapplied ?? changes;
20873
- const createdIds = finalChanges.activities.filter((change) => change.kind === "created").map((change) => String(change.id));
20874
- if (createdIds.length === 0) return reapplied;
20875
- const createdAndAncestors = new Set(createdIds);
20876
- const filterProjection = this.coreRuntime.state.getFilterProjection();
20877
- for (const createdId of createdIds) {
20878
- if (filterProjection !== null) {
20879
- filterProjection.pinnedVisibleIds.add(createdId);
20880
- }
20881
- let activityId = createdId;
20882
- while (activityId !== null && activityId !== ROOT_PARENT_ID) {
20883
- createdAndAncestors.add(activityId);
20884
- if (filterProjection !== null) {
20885
- filterProjection.visibleIds.add(activityId);
20886
- filterProjection.visibleReferenceCounts.set(
20887
- activityId,
20888
- (filterProjection.visibleReferenceCounts.get(activityId) ?? 0) + 1
20889
- );
20890
- }
20891
- activityId = this.coreRuntime.state.getParentId(activityId);
20892
- }
20893
- }
20894
- const createdVisibility = applyVisibleSet(
20895
- this.coreRuntime.state,
20896
- createdAndAncestors,
20897
- createdAndAncestors
20898
- );
20899
- if (createdVisibility.length === 0) return reapplied;
20900
- return {
20901
- ...finalChanges,
20902
- viewState: [...finalChanges.viewState ?? [], ...createdVisibility]
20903
- };
21841
+ return withOrder ?? sequenced.changes ?? withFilter;
20904
21842
  }
20905
21843
  /**
20906
21844
  * Emits `order` for the branches this mutation resequenced, when no user order
@@ -20979,16 +21917,22 @@ var ScheduleCore = class {
20979
21917
  context
20980
21918
  );
20981
21919
  const changedActivities = changesRequireFilterProjectionRebuild(changes) ? null : collectChangedActivities(changes, adapter);
21920
+ const relativePlacementPins = adapter.getRelativePlacementPins();
20982
21921
  let affectedIds;
20983
21922
  let visibleIds;
20984
- const needsProjectionRebuild = changedActivities === null || !projectionIsCurrent;
21923
+ const needsProjectionRebuild = changedActivities === null || !projectionIsCurrent || changesAffectRelativePinProjection(
21924
+ changes,
21925
+ changedActivities,
21926
+ adapter,
21927
+ relativePlacementPins
21928
+ );
20985
21929
  if (needsProjectionRebuild) {
20986
21930
  const rebuiltProjection = buildFilterProjection({
20987
21931
  activities: adapter.getAllActivities(),
20988
21932
  parentOf: (activityId) => adapter.getParentId(activityId),
20989
21933
  filter,
20990
21934
  context,
20991
- ...projectionIsCurrent ? { pinnedVisibleIds: projection.pinnedVisibleIds } : {}
21935
+ relativePlacementPins
20992
21936
  });
20993
21937
  adapter.setFilterProjection(rebuiltProjection);
20994
21938
  visibleIds = rebuiltProjection.visibleIds;
@@ -21062,13 +22006,20 @@ var ScheduleCore = class {
21062
22006
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
21063
22007
  let changes;
21064
22008
  try {
21065
- for (const [activityId, fields] of calculatedFields) {
22009
+ for (const [activityId, fields] of calculatedFields.activities) {
21066
22010
  setCriticalPathFieldsIfChanged(
21067
22011
  this.coreRuntime.state,
21068
22012
  activityId,
21069
22013
  fields
21070
22014
  );
21071
22015
  }
22016
+ for (const [linkId, fields] of calculatedFields.links) {
22017
+ setCriticalPathLinkFieldsIfChanged(
22018
+ this.coreRuntime.state,
22019
+ linkId,
22020
+ fields
22021
+ );
22022
+ }
21072
22023
  changes = await assembleChangeSet(this.coreRuntime.state, {
21073
22024
  source: { kind: "init" },
21074
22025
  beforeSnap: /* @__PURE__ */ new Map(),
@@ -21350,7 +22301,6 @@ var DISPATCH_ACTION_KIND = {
21350
22301
  FILTER_SET: "filter-set",
21351
22302
  SORT_SET: "sort-set",
21352
22303
  SIR_SYNC: "sir-sync",
21353
- SIR_APPROVE: "sir-approve",
21354
22304
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
21355
22305
  };
21356
22306
  var KIND_CATALOG_COVERS_UNION = {
@@ -21380,7 +22330,6 @@ var KIND_CATALOG_COVERS_UNION = {
21380
22330
  [DISPATCH_ACTION_KIND.FILTER_SET]: true,
21381
22331
  [DISPATCH_ACTION_KIND.SORT_SET]: true,
21382
22332
  [DISPATCH_ACTION_KIND.SIR_SYNC]: true,
21383
- [DISPATCH_ACTION_KIND.SIR_APPROVE]: true,
21384
22333
  [DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC]: true
21385
22334
  };
21386
22335
  var DISPATCH_ACTION_KINDS = Object.keys(KIND_CATALOG_COVERS_UNION);