@outbuild-company/schedule-core 1.6.3 → 1.8.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
@@ -134,6 +134,9 @@ var REJECTION_REASON = {
134
134
  ACTIVITY_NOT_FOUND: "activity_not_found",
135
135
  PARENT_NOT_FOUND: "parent_not_found",
136
136
  ACTIVITY_IDS_EMPTY: "activity_ids_empty",
137
+ NO_ACTIVITY_SELECTION: "no_activity_selection",
138
+ CLIPBOARD_EMPTY: "clipboard_empty",
139
+ ACTIVITY_PASTE_INVARIANT_FAILED: "activity_paste_invariant_failed",
137
140
  ANCHOR_SIBLING_CONFLICT: "anchor_sibling_conflict",
138
141
  ANCHOR_SIBLING_NOT_FOUND: "anchor_sibling_not_found",
139
142
  ANCHOR_SIBLING_WRONG_PARENT: "anchor_sibling_wrong_parent",
@@ -141,6 +144,8 @@ var REJECTION_REASON = {
141
144
  CANNOT_MOVE_INTO_OWN_DESCENDANT: "cannot_move_into_own_descendant",
142
145
  INDENT_NO_ELIGIBLE_SIBLING: "indent_no_eligible_sibling",
143
146
  OUTDENT_ROOT_LEVEL_NOT_EDITABLE: "outdent_root_level_not_editable",
147
+ HIERARCHY_CYCLE: "hierarchy_cycle",
148
+ SANDBOX_PERSISTENCE_FORBIDDEN: "sandbox_persistence_forbidden",
144
149
  INVALID_PROGRESS_VALUE: "invalid_progress_value_must_be_0_or_100"
145
150
  };
146
151
 
@@ -269,8 +274,17 @@ function toDays(hours, hoursPerDay) {
269
274
  return hours / hoursPerDay;
270
275
  }
271
276
  function calendarDaySpan(startDate, endDate) {
272
- const elapsed = endDate.getTime() - startDate.getTime();
273
- return Math.floor(elapsed / MILLISECONDS_PER_DAY) + 1;
277
+ const startDay = Date.UTC(
278
+ startDate.getUTCFullYear(),
279
+ startDate.getUTCMonth(),
280
+ startDate.getUTCDate()
281
+ );
282
+ const endDay = Date.UTC(
283
+ endDate.getUTCFullYear(),
284
+ endDate.getUTCMonth(),
285
+ endDate.getUTCDate()
286
+ );
287
+ return Math.floor((endDay - startDay) / MILLISECONDS_PER_DAY) + 1;
274
288
  }
275
289
  var FIELD_REGISTRY = {
276
290
  name: { valueKind: "string", extract: (activity) => activity.name },
@@ -285,7 +299,10 @@ var FIELD_REGISTRY = {
285
299
  },
286
300
  uniqueCorrelativeId: {
287
301
  valueKind: "number",
288
- extract: (activity) => Number(activity.uniqueCorrelativeId)
302
+ extract: (activity) => {
303
+ const value = Number(activity.uniqueCorrelativeId);
304
+ return Number.isFinite(value) ? value : null;
305
+ }
289
306
  },
290
307
  progress: { valueKind: "number", extract: (activity) => activity.progress },
291
308
  durationDays: {
@@ -294,7 +311,7 @@ var FIELD_REGISTRY = {
294
311
  },
295
312
  calendarDuration: {
296
313
  valueKind: "number",
297
- extract: (activity) => calendarDaySpan(activity.startDate, activity.endDate)
314
+ extract: (activity) => activity.type === "milestone" ? 0 : calendarDaySpan(activity.startDate, activity.endDate)
298
315
  },
299
316
  cost: { valueKind: "number", extract: (activity) => activity.cost },
300
317
  usedCost: { valueKind: "number", extract: (activity) => activity.usedCost },
@@ -660,34 +677,102 @@ var OPERATORS_BY_KIND = {
660
677
  boolean: /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
661
678
  reference: /* @__PURE__ */ new Set(["someOf", "notSomeOf"])
662
679
  };
663
- function validateCriterion(criterion) {
680
+ function normalizeDateRange(value) {
681
+ if (value === void 0) return { ok: true, dateRange: void 0 };
682
+ if (typeof value !== "object" || value === null) {
683
+ return { ok: false, reason: "invalid_value" };
684
+ }
685
+ const { start, end } = value;
686
+ if (!(start instanceof Date) || !(end instanceof Date)) {
687
+ return { ok: false, reason: "invalid_value" };
688
+ }
689
+ const startTime = start.getTime();
690
+ const endTime = end.getTime();
691
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime > endTime) {
692
+ return { ok: false, reason: "invalid_value" };
693
+ }
694
+ return {
695
+ ok: true,
696
+ dateRange: { start: new Date(startTime), end: new Date(endTime) }
697
+ };
698
+ }
699
+ function normalizeCriterion(criterion) {
664
700
  const descriptor = getFieldDescriptor(criterion.field);
665
- if (descriptor === null) return "unknown_field";
701
+ if (descriptor === null) return { ok: false, reason: "unknown_field" };
666
702
  if (!OPERATORS_BY_KIND[descriptor.valueKind].has(criterion.operator)) {
667
- return "unknown_operator";
703
+ return { ok: false, reason: "unknown_operator" };
704
+ }
705
+ if (descriptor.valueKind === "number") {
706
+ const value = normalizeNumberValue(criterion.value);
707
+ return value === null ? { ok: false, reason: "invalid_value" } : {
708
+ ok: true,
709
+ criterion: { ...criterion, value }
710
+ };
711
+ }
712
+ if (descriptor.valueKind === "date") {
713
+ const value = normalizeDateValue(criterion.value);
714
+ return value === null ? { ok: false, reason: "invalid_value" } : {
715
+ ok: true,
716
+ criterion: { ...criterion, value }
717
+ };
718
+ }
719
+ const rejection = validateValueForKind(criterion, descriptor.valueKind);
720
+ if (rejection !== null) return { ok: false, reason: rejection };
721
+ if (descriptor.valueKind === "boolean") {
722
+ return {
723
+ ok: true,
724
+ criterion: {
725
+ ...criterion,
726
+ value: [...new Set(criterion.value)]
727
+ }
728
+ };
729
+ }
730
+ if (descriptor.valueKind === "id-array" || descriptor.valueKind === "enum" || descriptor.valueKind === "reference") {
731
+ return {
732
+ ok: true,
733
+ criterion: {
734
+ ...criterion,
735
+ value: [
736
+ ...new Set(
737
+ criterion.value.map(String)
738
+ )
739
+ ]
740
+ }
741
+ };
668
742
  }
669
- return validateValueForKind(criterion, descriptor.valueKind);
743
+ return { ok: true, criterion };
670
744
  }
671
745
  function validateValueForKind(criterion, valueKind) {
672
746
  const { value } = criterion;
673
- if (valueKind === "id-array" || valueKind === "enum") {
674
- return Array.isArray(value) ? null : "invalid_value";
675
- }
676
- if (Array.isArray(value)) return "invalid_value";
677
- if (valueKind === "number") {
678
- return Number.isFinite(Number(value)) ? null : "invalid_value";
747
+ if (valueKind === "boolean") {
748
+ return Array.isArray(value) && value.every((item) => typeof item === "boolean") ? null : "invalid_value";
679
749
  }
680
- if (valueKind === "date") {
681
- return isValidDateValue(value) ? null : "invalid_value";
750
+ if (valueKind === "id-array" || valueKind === "enum" || valueKind === "reference") {
751
+ return Array.isArray(value) && value.every(
752
+ (item) => typeof item === "string" || typeof item === "number"
753
+ ) ? null : "invalid_value";
682
754
  }
755
+ if (Array.isArray(value)) return "invalid_value";
683
756
  return null;
684
757
  }
685
- function isValidDateValue(value) {
686
- if (value instanceof Date) return true;
687
- if (typeof value === "string" || typeof value === "number") {
688
- return !Number.isNaN(new Date(value).getTime());
689
- }
690
- return false;
758
+ function normalizeNumberValue(value) {
759
+ if (typeof value === "string" && value.trim() === "") return null;
760
+ if (typeof value !== "string" && typeof value !== "number") return null;
761
+ const parsed2 = typeof value === "number" ? value : Number(value);
762
+ return Number.isFinite(parsed2) ? parsed2 : null;
763
+ }
764
+ function normalizeDateValue(value) {
765
+ let date2;
766
+ if (value instanceof Date) date2 = new Date(value.getTime());
767
+ else if (typeof value === "string") date2 = new Date(value);
768
+ else if (typeof value === "number") date2 = new Date(value);
769
+ else return null;
770
+ return Number.isFinite(date2.getTime()) ? date2 : null;
771
+ }
772
+
773
+ // src/shared/clone-domain-value.ts
774
+ function cloneDomainValue(value) {
775
+ return structuredClone(value);
691
776
  }
692
777
 
693
778
  // src/dispatch/filter.ts
@@ -717,15 +802,21 @@ function resolveFilter(adapter, filter, hoursPerDay) {
717
802
  }
718
803
  function dispatchFilterSet(action, deps) {
719
804
  const { adapter, hoursPerDay } = deps;
805
+ const criteria = [];
720
806
  for (const criterion of action.criteria) {
721
- const rejection = validateCriterion(criterion);
722
- if (rejection !== null) return { ok: false, reason: rejection };
723
- }
724
- const filter = action.dateRange === void 0 ? { criteria: action.criteria, logic: action.logic } : {
725
- criteria: action.criteria,
726
- logic: action.logic,
727
- dateRange: action.dateRange
728
- };
807
+ const result = normalizeCriterion(criterion);
808
+ if (!result.ok) return { ok: false, reason: result.reason };
809
+ criteria.push(result.criterion);
810
+ }
811
+ const rangeResult = normalizeDateRange(action.dateRange);
812
+ if (!rangeResult.ok) return { ok: false, reason: rangeResult.reason };
813
+ const filter = cloneDomainValue(
814
+ rangeResult.dateRange === void 0 ? { criteria, logic: action.logic } : {
815
+ criteria,
816
+ logic: action.logic,
817
+ dateRange: rangeResult.dateRange
818
+ }
819
+ );
729
820
  adapter.setActiveFilter(isEmptyFilter(filter) ? null : filter);
730
821
  const { visibleIds, projection } = resolveFilter(
731
822
  adapter,
@@ -870,7 +961,7 @@ function dispatchSortSet(action, deps) {
870
961
  const rejection = validateRule(rule);
871
962
  if (rejection !== null) return { ok: false, reason: rejection };
872
963
  }
873
- const order = action.rules.length === 0 ? null : { rules: action.rules };
964
+ const order = action.rules.length === 0 ? null : cloneDomainValue({ rules: action.rules });
874
965
  adapter.setActiveOrder(order);
875
966
  const changes = {
876
967
  source: action,
@@ -1438,6 +1529,11 @@ function isDescendantOf(childId, ancestorId, adapter) {
1438
1529
  }
1439
1530
  return false;
1440
1531
  }
1532
+ function isSummaryHierarchyLink(source, target, adapter) {
1533
+ const sourceIsSummary = isSummary2(source, adapter);
1534
+ const targetIsSummary = isSummary2(target, adapter);
1535
+ return targetIsSummary && isDescendantOf(source, target, adapter) || sourceIsSummary && isDescendantOf(target, source, adapter);
1536
+ }
1441
1537
  function getLeafDescendants(parentId, adapter) {
1442
1538
  const leaves = [];
1443
1539
  const stack = [parentId];
@@ -1696,6 +1792,8 @@ function buildVirtualLink(originalLink, sourceId, targetId, counter, sourceLag,
1696
1792
  target: targetId,
1697
1793
  type: originalLink.type,
1698
1794
  lag: originalLink.lag,
1795
+ freeSlackHours: null,
1796
+ isCritical: false,
1699
1797
  _sourceLag: sourceLag,
1700
1798
  _targetLag: targetLag,
1701
1799
  _trueLag: trueLag,
@@ -2375,6 +2473,18 @@ var noopReporter = {
2375
2473
  }
2376
2474
  };
2377
2475
 
2476
+ // src/internal/state/calendar-duration.ts
2477
+ var CALENDAR_DAY_MS = 864e5;
2478
+ function computeCalendarDuration(activity) {
2479
+ if (activity.type === "milestone") return 0;
2480
+ const start = activity.startDate?.getTime();
2481
+ const end = activity.endDate?.getTime();
2482
+ if (start == null || end == null || Number.isNaN(start) || Number.isNaN(end)) {
2483
+ return null;
2484
+ }
2485
+ return Math.floor((end - start) / CALENDAR_DAY_MS) + 1;
2486
+ }
2487
+
2378
2488
  // src/autoscheduler/integration/apply-results.ts
2379
2489
  function applyResults(adapter, result) {
2380
2490
  if (result.cancelled || result.updatedIds.length === 0) return;
@@ -2386,6 +2496,7 @@ function applyResults(adapter, result) {
2386
2496
  if (!liveActivity) continue;
2387
2497
  liveActivity.startDate = plan.startDate;
2388
2498
  liveActivity.endDate = plan.endDate;
2499
+ liveActivity.calendarDuration = computeCalendarDuration(liveActivity);
2389
2500
  adapter.updateActivity(id);
2390
2501
  }
2391
2502
  });
@@ -2452,13 +2563,253 @@ function hasPath(adjacency, from, to) {
2452
2563
  return false;
2453
2564
  }
2454
2565
 
2566
+ // src/internal/dependency-graph/strongly-connected-components.ts
2567
+ function circularComponents(edges) {
2568
+ const walk = buildWalkState(edges);
2569
+ for (const node of walk.adjacency.keys()) {
2570
+ if (!walk.records.has(node)) collectFromRoot(node, walk);
2571
+ }
2572
+ return walk.components;
2573
+ }
2574
+ function buildWalkState(edges) {
2575
+ const adjacency = /* @__PURE__ */ new Map();
2576
+ const selfLooped = /* @__PURE__ */ new Set();
2577
+ for (const edge of edges) {
2578
+ const source = String(edge.source);
2579
+ const target = String(edge.target);
2580
+ const sourceNeighbors = neighborsOf(adjacency, source);
2581
+ neighborsOf(adjacency, target);
2582
+ if (source === target) {
2583
+ selfLooped.add(source);
2584
+ continue;
2585
+ }
2586
+ sourceNeighbors.push(target);
2587
+ }
2588
+ return {
2589
+ adjacency,
2590
+ selfLooped,
2591
+ records: /* @__PURE__ */ new Map(),
2592
+ componentStack: [],
2593
+ nextIndex: 0,
2594
+ components: []
2595
+ };
2596
+ }
2597
+ function neighborsOf(adjacency, node) {
2598
+ const existing = adjacency.get(node);
2599
+ if (existing) return existing;
2600
+ const created = [];
2601
+ adjacency.set(node, created);
2602
+ return created;
2603
+ }
2604
+ function collectFromRoot(root, walk) {
2605
+ const frames = [];
2606
+ frames.push({ node: root, record: enterNode(root, walk), neighborCursor: 0 });
2607
+ while (frames.length > 0) {
2608
+ const frame = frames[frames.length - 1];
2609
+ if (frame === void 0) break;
2610
+ const neighbors = walk.adjacency.get(frame.node) ?? [];
2611
+ if (frame.neighborCursor < neighbors.length) {
2612
+ const neighbor = neighbors[frame.neighborCursor];
2613
+ frame.neighborCursor += 1;
2614
+ if (neighbor === void 0) continue;
2615
+ visitNeighbor(frame, neighbor, frames, walk);
2616
+ continue;
2617
+ }
2618
+ frames.pop();
2619
+ const parentFrame = frames[frames.length - 1];
2620
+ if (parentFrame && frame.record.lowLink < parentFrame.record.lowLink) {
2621
+ parentFrame.record.lowLink = frame.record.lowLink;
2622
+ }
2623
+ const isComponentRoot = frame.record.lowLink === frame.record.visitIndex;
2624
+ if (isComponentRoot) emitComponent(frame.node, walk);
2625
+ }
2626
+ }
2627
+ function visitNeighbor(frame, neighbor, frames, walk) {
2628
+ const neighborRecord = walk.records.get(neighbor);
2629
+ if (!neighborRecord) {
2630
+ frames.push({
2631
+ node: neighbor,
2632
+ record: enterNode(neighbor, walk),
2633
+ neighborCursor: 0
2634
+ });
2635
+ return;
2636
+ }
2637
+ const lowersCurrent = neighborRecord.onStack && neighborRecord.visitIndex < frame.record.lowLink;
2638
+ if (lowersCurrent) frame.record.lowLink = neighborRecord.visitIndex;
2639
+ }
2640
+ function enterNode(node, walk) {
2641
+ const record = {
2642
+ visitIndex: walk.nextIndex,
2643
+ lowLink: walk.nextIndex,
2644
+ onStack: true
2645
+ };
2646
+ walk.nextIndex += 1;
2647
+ walk.records.set(node, record);
2648
+ walk.componentStack.push({ node, record });
2649
+ return record;
2650
+ }
2651
+ function emitComponent(rootNode, walk) {
2652
+ const component = [];
2653
+ for (; ; ) {
2654
+ const entry = walk.componentStack.pop();
2655
+ if (entry === void 0) break;
2656
+ entry.record.onStack = false;
2657
+ component.push(entry.node);
2658
+ if (entry.node === rootNode) break;
2659
+ }
2660
+ const firstMember = component[0];
2661
+ const isSingleSelfLoop = component.length === 1 && firstMember !== void 0 && walk.selfLooped.has(firstMember);
2662
+ const isCircular = component.length > 1 || isSingleSelfLoop;
2663
+ if (isCircular) walk.components.push(component);
2664
+ }
2665
+
2666
+ // src/internal/dependency-graph/effective-graph.ts
2667
+ function projectEffectiveEdges(port, links, proposal) {
2668
+ const projectionPort = proposal ? new ReparentedPort(port, proposal) : port;
2669
+ return expandParentLinks([...links], projectionPort);
2670
+ }
2671
+ var ReparentedPort = class {
2672
+ constructor(base, proposal) {
2673
+ this.base = base;
2674
+ for (const [childId, proposedParent] of proposal.reparented) {
2675
+ const childKey = String(childId);
2676
+ const newParent = proposedParent === null || isRootParent(proposedParent) ? null : proposedParent;
2677
+ const oldParent = base.getActivity(childId)?.parentId ?? null;
2678
+ const oldKey = oldParent === null || isRootParent(oldParent) ? null : String(oldParent);
2679
+ const newKey = newParent === null ? null : String(newParent);
2680
+ if (oldKey === newKey) continue;
2681
+ this.reparented.set(childKey, newParent);
2682
+ if (oldKey !== null) {
2683
+ const removed = this.movedOutByParent.get(oldKey) ?? /* @__PURE__ */ new Set();
2684
+ removed.add(childKey);
2685
+ this.movedOutByParent.set(oldKey, removed);
2686
+ }
2687
+ if (newKey !== null) {
2688
+ const gained = this.movedInByParent.get(newKey) ?? [];
2689
+ gained.push(childId);
2690
+ this.movedInByParent.set(newKey, gained);
2691
+ }
2692
+ }
2693
+ }
2694
+ base;
2695
+ reparented = /* @__PURE__ */ new Map();
2696
+ movedOutByParent = /* @__PURE__ */ new Map();
2697
+ movedInByParent = /* @__PURE__ */ new Map();
2698
+ withOverride(activity) {
2699
+ if (!activity) return null;
2700
+ const override = this.reparented.get(String(activity.id));
2701
+ if (override === void 0) return activity;
2702
+ return { ...activity, parentId: override };
2703
+ }
2704
+ getActivity(activityId) {
2705
+ return this.withOverride(this.base.getActivity(activityId));
2706
+ }
2707
+ getLiveActivity(activityId) {
2708
+ return this.withOverride(this.base.getLiveActivity(activityId));
2709
+ }
2710
+ getAllActivities() {
2711
+ return this.base.getAllActivities().map((activity) => this.withOverride(activity) ?? activity);
2712
+ }
2713
+ getChildren(parentId) {
2714
+ const parentKey = String(parentId);
2715
+ const removed = this.movedOutByParent.get(parentKey);
2716
+ const baseChildren = this.base.getChildren(parentId);
2717
+ const kept = removed ? baseChildren.filter((childId) => !removed.has(String(childId))) : [...baseChildren];
2718
+ const gained = this.movedInByParent.get(parentKey);
2719
+ if (gained) kept.push(...gained);
2720
+ return kept;
2721
+ }
2722
+ getParent(activityId) {
2723
+ const override = this.reparented.get(String(activityId));
2724
+ if (override === void 0) return this.base.getParent(activityId);
2725
+ if (override === null) return null;
2726
+ return this.getActivity(override);
2727
+ }
2728
+ isChildOf(childId, parentId) {
2729
+ const targetKey = String(parentId);
2730
+ const visited = /* @__PURE__ */ new Set();
2731
+ let current = this.getActivity(childId);
2732
+ while (current) {
2733
+ const parent = current.parentId;
2734
+ if (parent === null || isRootParent(parent)) return false;
2735
+ const parentKey = String(parent);
2736
+ if (parentKey === targetKey) return true;
2737
+ if (visited.has(parentKey)) return false;
2738
+ visited.add(parentKey);
2739
+ current = this.getActivity(parent);
2740
+ }
2741
+ return false;
2742
+ }
2743
+ getAllLinks() {
2744
+ return this.base.getAllLinks();
2745
+ }
2746
+ getAllIds() {
2747
+ return this.base.getAllIds();
2748
+ }
2749
+ getLink(linkId) {
2750
+ return this.base.getLink(linkId);
2751
+ }
2752
+ activityExists(activityId) {
2753
+ return this.base.activityExists(activityId);
2754
+ }
2755
+ getOutgoingLinkIds(activityId) {
2756
+ return this.base.getOutgoingLinkIds(activityId);
2757
+ }
2758
+ getIncomingLinkIds(activityId) {
2759
+ return this.base.getIncomingLinkIds(activityId);
2760
+ }
2761
+ getClosestWorkTime(params) {
2762
+ return this.base.getClosestWorkTime(params);
2763
+ }
2764
+ calculateEndDate(params) {
2765
+ return this.base.calculateEndDate(params);
2766
+ }
2767
+ calculateDuration(params) {
2768
+ return this.base.calculateDuration(params);
2769
+ }
2770
+ batchUpdate(runBatch) {
2771
+ this.base.batchUpdate(runBatch);
2772
+ }
2773
+ updateActivity(activityId) {
2774
+ this.base.updateActivity(activityId);
2775
+ }
2776
+ getFlags() {
2777
+ return this.base.getFlags();
2778
+ }
2779
+ getProjectEnd() {
2780
+ return this.base.getProjectEnd();
2781
+ }
2782
+ };
2783
+
2784
+ // src/internal/dependency-graph/effective-graph-validation.ts
2785
+ function validateTopologyCommit(input) {
2786
+ const { port, currentLinks, proposedLinks, proposal } = input;
2787
+ const proposedEdges = projectEffectiveEdges(port, proposedLinks, proposal);
2788
+ if (!hasDirectedCycle(proposedEdges)) return { ok: true };
2789
+ const currentEdges = projectEffectiveEdges(port, currentLinks);
2790
+ const corruptBefore = corruptActivityIds(currentEdges);
2791
+ const corruptAfter = corruptActivityIds(proposedEdges);
2792
+ const newlyCyclicActivityIds = [...corruptAfter].filter(
2793
+ (activityId) => !corruptBefore.has(activityId)
2794
+ );
2795
+ if (newlyCyclicActivityIds.length === 0) return { ok: true };
2796
+ return { ok: false, newlyCyclicActivityIds };
2797
+ }
2798
+ function corruptActivityIds(edges) {
2799
+ const corrupt = /* @__PURE__ */ new Set();
2800
+ for (const component of circularComponents(edges)) {
2801
+ for (const activityId of component) corrupt.add(activityId);
2802
+ }
2803
+ return corrupt;
2804
+ }
2805
+
2455
2806
  // src/autoscheduler/links/apply-link-operation.ts
2456
2807
  function applyLinkOperation(op, deps) {
2457
2808
  switch (op.kind) {
2458
2809
  case "create":
2459
2810
  return applyCreate(op, deps);
2460
2811
  case "update":
2461
- return applyUpdate(op, deps);
2812
+ return applyLinkUpdate(op, deps.port);
2462
2813
  case "delete":
2463
2814
  return applyDelete(op, deps);
2464
2815
  }
@@ -2484,6 +2835,10 @@ function applyLinkCreates(links, deps) {
2484
2835
  rejected = { applied: false, rejected: "activity-missing" };
2485
2836
  break;
2486
2837
  }
2838
+ if (isSummaryHierarchyLink(operation.source, operation.target, deps.port)) {
2839
+ rejected = { applied: false, rejected: "hierarchy-link" };
2840
+ break;
2841
+ }
2487
2842
  const key = linkKey(operation);
2488
2843
  const duplicateId = duplicateKeys.get(key);
2489
2844
  if (duplicateId !== void 0) {
@@ -2509,6 +2864,26 @@ function applyLinkCreates(links, deps) {
2509
2864
  return { applied: false, rejected: "cycle" };
2510
2865
  }
2511
2866
  if (rejected) return rejected;
2867
+ const proposedLinks = [
2868
+ ...existingLinks,
2869
+ ...links.map(({ operation, linkId }) => ({
2870
+ id: linkId,
2871
+ source: operation.source,
2872
+ target: operation.target,
2873
+ type: operation.type,
2874
+ lag: operation.lag,
2875
+ freeSlackHours: null,
2876
+ isCritical: false
2877
+ }))
2878
+ ];
2879
+ const effectiveVerdict = validateTopologyCommit({
2880
+ port: deps.port,
2881
+ currentLinks: existingLinks,
2882
+ proposedLinks
2883
+ });
2884
+ if (!effectiveVerdict.ok) {
2885
+ return { applied: false, rejected: "cycle" };
2886
+ }
2512
2887
  for (const { operation, linkId } of links) {
2513
2888
  const preserved = deps.preserved?.get(linkId);
2514
2889
  deps.port.addLink({
@@ -2517,6 +2892,8 @@ function applyLinkCreates(links, deps) {
2517
2892
  target: operation.target,
2518
2893
  type: operation.type,
2519
2894
  lag: operation.lag,
2895
+ freeSlackHours: null,
2896
+ isCritical: false,
2520
2897
  ...preserved?.ganttId === void 0 ? {} : { ganttId: preserved.ganttId },
2521
2898
  ...preserved?.sectorId === void 0 ? {} : { sectorId: preserved.sectorId },
2522
2899
  ...preserved?.proplannerId === void 0 ? {} : { proplannerId: preserved.proplannerId }
@@ -2537,6 +2914,9 @@ function applyCreate(op, deps) {
2537
2914
  if (!src || !tgt) {
2538
2915
  return { applied: false, rejected: "activity-missing" };
2539
2916
  }
2917
+ if (isSummaryHierarchyLink(op.source, op.target, port)) {
2918
+ return { applied: false, rejected: "hierarchy-link" };
2919
+ }
2540
2920
  const existingLinks = port.getAllLinks();
2541
2921
  for (const l of existingLinks) {
2542
2922
  if (String(l.source) === String(op.source) && String(l.target) === String(op.target) && l.type === op.type) {
@@ -2556,13 +2936,22 @@ function applyCreate(op, deps) {
2556
2936
  source: op.source,
2557
2937
  target: op.target,
2558
2938
  type: op.type,
2559
- lag: op.lag
2939
+ lag: op.lag,
2940
+ freeSlackHours: null,
2941
+ isCritical: false
2560
2942
  };
2943
+ const effectiveVerdict = validateTopologyCommit({
2944
+ port,
2945
+ currentLinks: existingLinks,
2946
+ proposedLinks: [...existingLinks, link]
2947
+ });
2948
+ if (!effectiveVerdict.ok) {
2949
+ return { applied: false, rejected: "cycle" };
2950
+ }
2561
2951
  port.addLink(link);
2562
2952
  return { applied: true, linkId: id };
2563
2953
  }
2564
- function applyUpdate(op, deps) {
2565
- const { port } = deps;
2954
+ function applyLinkUpdate(op, port) {
2566
2955
  const link = port.getLink(op.linkId);
2567
2956
  if (!link) return { applied: false, rejected: "link-missing" };
2568
2957
  if (op.lag !== void 0) port.setLinkField(op.linkId, "lag", op.lag);
@@ -2733,6 +3122,10 @@ function diffOutgoingLinks(input) {
2733
3122
  }
2734
3123
 
2735
3124
  // src/autoscheduler/links/lag-units.ts
3125
+ function roundLagDays(lagDays) {
3126
+ const rounded = Math.sign(lagDays) * Math.round(Math.abs(lagDays));
3127
+ return Object.is(rounded, -0) ? 0 : rounded;
3128
+ }
2736
3129
  function lagDaysToHours(lagDays, hoursPerDay) {
2737
3130
  return Math.sign(lagDays) * Math.round(Math.abs(lagDays) * hoursPerDay);
2738
3131
  }
@@ -2995,10 +3388,7 @@ function adjustLinkLagOnTaskMove(targetId, adapter, preEditTarget) {
2995
3388
  calendarApi
2996
3389
  );
2997
3390
  if (newLag === link.lag) continue;
2998
- applyLinkOperation(
2999
- { kind: "update", linkId: link.id, lag: newLag },
3000
- { port: adapter }
3001
- );
3391
+ applyLinkUpdate({ linkId: link.id, lag: newLag }, adapter);
3002
3392
  }
3003
3393
  }
3004
3394
 
@@ -3062,19 +3452,44 @@ async function recomputeParentsCooperatively(parents, adapter, progressMode, opt
3062
3452
  }
3063
3453
  function findAncestorsOfDirty(adapter, dirtyIds) {
3064
3454
  const ancestors = /* @__PURE__ */ new Set();
3065
- for (const id of dirtyIds) walkAncestors(id, adapter, ancestors);
3455
+ const parentById = /* @__PURE__ */ new Map();
3456
+ for (const id of dirtyIds) {
3457
+ walkAncestors(id, adapter, ancestors, parentById);
3458
+ }
3066
3459
  if (ancestors.size === 0) return [];
3067
3460
  const ids = Array.from(ancestors);
3068
- const depths = computeDepths(ids, adapter);
3461
+ const depths = computeKnownDepths(ids, parentById);
3069
3462
  return sortDeepestFirst(ids, depths);
3070
3463
  }
3071
- function walkAncestors(id, adapter, collector) {
3464
+ function walkAncestors(id, adapter, collector, parentById) {
3072
3465
  let current = adapter.getParentId(id);
3073
3466
  while (current && !isRootParent(current)) {
3074
3467
  if (collector.has(current)) return;
3075
3468
  collector.add(current);
3076
- current = adapter.getParentId(current);
3469
+ const parent = adapter.getParentId(current);
3470
+ parentById.set(current, parent);
3471
+ current = parent;
3472
+ }
3473
+ }
3474
+ function computeKnownDepths(ids, parentById) {
3475
+ const depths = /* @__PURE__ */ new Map();
3476
+ for (const id of ids) {
3477
+ if (depths.has(id)) continue;
3478
+ const path = [];
3479
+ const inPath = /* @__PURE__ */ new Set();
3480
+ let current = id;
3481
+ while (current && !isRootParent(current) && !depths.has(current) && !inPath.has(current)) {
3482
+ path.push(current);
3483
+ inPath.add(current);
3484
+ current = parentById.get(current) ?? null;
3485
+ }
3486
+ let depth = current ? depths.get(current) ?? 0 : 0;
3487
+ for (let index = path.length - 1; index >= 0; index -= 1) {
3488
+ depth += 1;
3489
+ depths.set(path[index], depth);
3490
+ }
3077
3491
  }
3492
+ return depths;
3078
3493
  }
3079
3494
  function aggregateChildren(childrenIds, adapter, includeRealWork) {
3080
3495
  let minStart = null;
@@ -3305,7 +3720,7 @@ var setEndDate = (value) => ({
3305
3720
 
3306
3721
  // src/columns/shared/date-normalize.ts
3307
3722
  function truncateToUtcDay(date2, tz) {
3308
- if (tz) {
3723
+ {
3309
3724
  const parts = new Intl.DateTimeFormat("en-CA", {
3310
3725
  timeZone: tz,
3311
3726
  year: "numeric",
@@ -3315,9 +3730,6 @@ function truncateToUtcDay(date2, tz) {
3315
3730
  const get = (kind) => Number(parts.find((p) => p.type === kind)?.value);
3316
3731
  return new Date(Date.UTC(get("year"), get("month") - 1, get("day")));
3317
3732
  }
3318
- return new Date(
3319
- Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate())
3320
- );
3321
3733
  }
3322
3734
  function endOfLocalDay(date2, tz) {
3323
3735
  {
@@ -3327,11 +3739,11 @@ function endOfLocalDay(date2, tz) {
3327
3739
  }
3328
3740
  }
3329
3741
  function nextWorkingDay(calendar, domainDay) {
3330
- let cursor = truncateToUtcDay(domainDay);
3742
+ let cursor = truncateToUtcDay(domainDay, "UTC");
3331
3743
  while (!calendar.isWorkTime(cursor, CALENDAR_UNIT.DAY)) {
3332
3744
  cursor = new Date(cursor);
3333
3745
  cursor.setUTCDate(cursor.getUTCDate() + 1);
3334
- cursor = truncateToUtcDay(cursor);
3746
+ cursor = truncateToUtcDay(cursor, "UTC");
3335
3747
  }
3336
3748
  return cursor;
3337
3749
  }
@@ -3369,11 +3781,12 @@ var DATELESS_CONSTRAINTS = /* @__PURE__ */ new Set([
3369
3781
  ]);
3370
3782
  var IMPLIED_CONSTRAINT_ON_DATE_EDIT = "snet";
3371
3783
  function normalizeConstraintDate(typedDay, calendar, constraintType) {
3784
+ const constraintDay = truncateToUtcDay(typedDay, "UTC");
3372
3785
  if (!START_BASED_CONSTRAINTS2.has(constraintType) && !END_BASED_CONSTRAINTS.has(constraintType)) {
3373
- return typedDay;
3786
+ return constraintDay;
3374
3787
  }
3375
- const reference = START_BASED_CONSTRAINTS2.has(constraintType) ? shiftStart(calendar, typedDay) : shiftEnd(calendar, typedDay);
3376
- const result = new Date(typedDay);
3788
+ const reference = START_BASED_CONSTRAINTS2.has(constraintType) ? shiftStart(calendar, constraintDay) : shiftEnd(calendar, constraintDay);
3789
+ const result = new Date(constraintDay);
3377
3790
  result.setUTCHours(
3378
3791
  reference.getUTCHours(),
3379
3792
  reference.getUTCMinutes(),
@@ -3429,10 +3842,8 @@ var durationPipeline = {
3429
3842
  },
3430
3843
  parseInput(rawValue, _activity, ctx) {
3431
3844
  const numericValue = parseFloat(String(rawValue));
3432
- if (Number.isNaN(numericValue)) return parseError("not_a_number");
3433
- return parsed(
3434
- ctx.inputUnit === "hours" ? numericValue : durationToHours(numericValue, ctx.hoursPerDay)
3435
- );
3845
+ if (!Number.isFinite(numericValue)) return parseError("not_a_number");
3846
+ return parsed(durationToHours(numericValue, ctx.hoursPerDay));
3436
3847
  },
3437
3848
  validate(_activity, oldValue, newValue) {
3438
3849
  if (isNegativeDuration(newValue)) {
@@ -3881,17 +4292,7 @@ var startDatePipeline = {
3881
4292
  transform(activity, newValue, ctx) {
3882
4293
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
3883
4294
  if (!calendar) {
3884
- return fieldChanges(
3885
- {
3886
- startDate: newValue,
3887
- ...setConstraintTypeImplied(),
3888
- ...setConstraintDate(newValue)
3889
- },
3890
- {
3891
- autoSchedule: true,
3892
- postProcessors: ["adjustLinkLagOnTaskMove"]
3893
- }
3894
- );
4295
+ throw new Error("startDatePipeline: no calendar resolved (integrity)");
3895
4296
  }
3896
4297
  const finalStart = shiftStart(calendar, newValue);
3897
4298
  const milestoneActivity = activity.type === "milestone";
@@ -3921,7 +4322,7 @@ function checkParentStartConstraint(activity, newDate, ctx) {
3921
4322
  activity.id
3922
4323
  );
3923
4324
  if (!parentStartDate) return null;
3924
- const parentDay = truncateToUtcDay(parentStartDate);
4325
+ const parentDay = truncateToUtcDay(parentStartDate, "UTC");
3925
4326
  if (newDate.getTime() < parentDay.getTime()) {
3926
4327
  return invalid("before_parent_start");
3927
4328
  }
@@ -3969,7 +4370,6 @@ var endDatePipeline = {
3969
4370
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
3970
4371
  if (!calendar) return parsed(day);
3971
4372
  if (!calendar.isWorkTime(day, CALENDAR_UNIT.DAY)) {
3972
- if (ctx.dateFormat.split(" ").length > 1) return parsed(day);
3973
4373
  return parsed(
3974
4374
  calendar.getClosestWorkTime({
3975
4375
  direction: "past",
@@ -4152,7 +4552,12 @@ var constraintDatePipeline = {
4152
4552
  transform(activity, newValue, ctx) {
4153
4553
  const finalType = isUnconstrained(activity) ? IMPLIED_CONSTRAINT_ON_DATE_EDIT : effectiveConstraintType(activity);
4154
4554
  const calendar = ctx.calendars.getCalendar(activity.calendarId);
4155
- const storedDate = calendar ? normalizeConstraintDate(newValue, calendar, finalType) : newValue;
4555
+ if (!calendar) {
4556
+ throw new Error(
4557
+ "constraintDatePipeline: no calendar resolved (integrity)"
4558
+ );
4559
+ }
4560
+ const storedDate = normalizeConstraintDate(newValue, calendar, finalType);
4156
4561
  const patch = buildConstraintDateExtraFields(activity, storedDate);
4157
4562
  return fieldChanges(patch, {
4158
4563
  autoSchedule: true
@@ -4452,6 +4857,31 @@ var COLUMN_WRITABLE_FIELDS = new Set(
4452
4857
  COLUMN_WRITABLE_FIELD_LIST
4453
4858
  );
4454
4859
 
4860
+ // src/constants/activity-properties.ts
4861
+ var ACTIVITY_PROPERTY = {
4862
+ PARENT: "parentId",
4863
+ TYPE: "type",
4864
+ START_DATE: "startDate",
4865
+ END_DATE: "endDate",
4866
+ CONSTRAINT_TYPE: "constraintType",
4867
+ CONSTRAINT_DATE: "constraintDate",
4868
+ PROGRESS: "progress",
4869
+ COST: "cost",
4870
+ USED_COST: "usedCost",
4871
+ REAL_COST: "realCost",
4872
+ WORK_HOURS: "workHours",
4873
+ REAL_WORK_HOURS: "realWorkHours",
4874
+ CORRELATIVE_ID: "correlativeId",
4875
+ CUSTOM_ID: "customId",
4876
+ NEW_ACTIVITIES_ARRAY: "newActivityIds",
4877
+ HAS_NEW_ACTIVITIES: "hasNewActivities",
4878
+ CALENDAR_DURATION: "calendarDuration"};
4879
+ var LINK_PROPERTY = {
4880
+ TYPE: "type",
4881
+ LAG: "lag",
4882
+ FREE_SLACK_HOURS: "freeSlackHours",
4883
+ IS_CRITICAL: "isCritical"};
4884
+
4455
4885
  // src/internal/state/dynamic-writes.ts
4456
4886
  function setActivityFieldDynamic(state, activityId, field, value) {
4457
4887
  state.setActivityFieldDynamic(activityId, field, value);
@@ -4503,11 +4933,6 @@ function cloneCriticalPath(value) {
4503
4933
  };
4504
4934
  }
4505
4935
 
4506
- // src/shared/clone-domain-value.ts
4507
- function cloneDomainValue(value) {
4508
- return structuredClone(value);
4509
- }
4510
-
4511
4936
  // src/dispatch/shared/snapshots.ts
4512
4937
  var YIELD_EVERY_N_ENTRIES = 200;
4513
4938
  function collectTouchedIds(primary, changes) {
@@ -4619,6 +5044,7 @@ function diffActivity(before, after) {
4619
5044
  const beforeRec = before ?? {};
4620
5045
  const afterRec = after;
4621
5046
  const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRec), ...Object.keys(afterRec)]);
5047
+ keys.delete(ACTIVITY_PROPERTY.CALENDAR_DURATION);
4622
5048
  for (const k of keys) {
4623
5049
  if (Object.hasOwn(beforeRec, k) !== Object.hasOwn(afterRec, k) || !fieldValueEqual(beforeRec[k], afterRec[k])) {
4624
5050
  fields[k] = {
@@ -5310,10 +5736,11 @@ function mutateTheDateToFutureOrPastInBaseRestriction(dateBaseToCalculate, restr
5310
5736
 
5311
5737
  // src/critical-path/legacy/base/parents-calculations/forward.js
5312
5738
  var CalculateForwardParentsWithLinks = class {
5313
- constructor(linkedActivitiesData, activity, gantt) {
5739
+ constructor(linkedActivitiesData, activity, gantt, linkCandidates) {
5314
5740
  this.linkedActivitiesData = linkedActivitiesData;
5315
5741
  this.activity = activity;
5316
5742
  this.gantt = gantt;
5743
+ this.linkCandidates = linkCandidates;
5317
5744
  this.activityCalendar = this.gantt.getCalendar(this.activity.calendar_id);
5318
5745
  if (!this.activityCalendar) {
5319
5746
  throw new Error(
@@ -5358,6 +5785,10 @@ var CalculateForwardParentsWithLinks = class {
5358
5785
  continue;
5359
5786
  }
5360
5787
  if (calculation) {
5788
+ this.linkCandidates?.set(linkedActivity.linkId, {
5789
+ targetId: String(this.activity.id),
5790
+ ...calculation
5791
+ });
5361
5792
  calculations.push(calculation);
5362
5793
  }
5363
5794
  }
@@ -5805,7 +6236,8 @@ function parentWithLinks(params) {
5805
6236
  linkDirection,
5806
6237
  calculations,
5807
6238
  gantt,
5808
- direction
6239
+ direction,
6240
+ linkCandidates
5809
6241
  } = params;
5810
6242
  const linksArray = parent[linkProperty];
5811
6243
  const parentData = extractParentData(parent);
@@ -5820,7 +6252,8 @@ function parentWithLinks(params) {
5820
6252
  calculation = new forward_default(
5821
6253
  linksWithActivityData,
5822
6254
  parent,
5823
- gantt
6255
+ gantt,
6256
+ linkCandidates
5824
6257
  );
5825
6258
  calculation = calculation.calculate();
5826
6259
  } else {
@@ -5865,6 +6298,7 @@ function getLinksWithActivityDataForward(linksArray, linkDirection, calculations
5865
6298
  return null;
5866
6299
  }
5867
6300
  return {
6301
+ linkId: String(linkData.id),
5868
6302
  es: calculationsOfLinkedActivity.es,
5869
6303
  ef: calculationsOfLinkedActivity.ef,
5870
6304
  linkLag: linkData.lag,
@@ -5965,6 +6399,7 @@ var CalculationOfParent = class {
5965
6399
  this.calculationOfParentsFromLinks = params.calculationOfParentsFromLinks;
5966
6400
  this.singleParents = params.singleParents;
5967
6401
  this.parentCalculationFromChildren = params.parentCalculationFromChildren;
6402
+ this.linkCandidates = params.linkCandidates;
5968
6403
  this.gantt = params.gantt;
5969
6404
  }
5970
6405
  handleParentWithLinks() {
@@ -6012,7 +6447,8 @@ var CalculationOfParent = class {
6012
6447
  linkDirection: this.linkDirection,
6013
6448
  gantt: this.gantt,
6014
6449
  calculations: this.calculations,
6015
- direction: this.direction
6450
+ direction: this.direction,
6451
+ linkCandidates: this.linkCandidates
6016
6452
  });
6017
6453
  this.calculationsOfParentThatImpactChildrens.set(activity.id, calculation);
6018
6454
  this.calculationOfParentsFromLinks.add(activity.id);
@@ -6661,6 +7097,7 @@ var CriticalPathHelpers = class {
6661
7097
  calculationOfParentsFromLinks: this.calculationOfParentsFromLinks,
6662
7098
  singleParents: this.singleParents,
6663
7099
  parentCalculationFromChildren: this.parentCalculationFromChildren,
7100
+ linkCandidates: this.linkCandidates,
6664
7101
  gantt: this.gantt
6665
7102
  });
6666
7103
  if (this.pendingParentsWithLinks.has(activity.id)) {
@@ -6728,6 +7165,7 @@ var LinksRulesCalculationsMethods = class {
6728
7165
  this.parentsWithLinks = calculationObject.parentsWithLinks;
6729
7166
  this.parentsCalculations = calculationObject.parentsCalculations;
6730
7167
  this.gantt = calculationObject.ganttInstance;
7168
+ this.linkCandidates = calculationObject.linkCandidates;
6731
7169
  }
6732
7170
  getEsAndEfFromPreviousCalculatedActivities(activity) {
6733
7171
  const isAlap = this.alapMap.has(activity);
@@ -6899,7 +7337,12 @@ var LinksConstraintCalculator = class extends LinksRulesCalculationsMethods_defa
6899
7337
  if (!linkData) {
6900
7338
  throw new Error("Link data not found", { cause: { linkId: link } });
6901
7339
  }
6902
- return this.calculateLink(linkData);
7340
+ const calculation = this.calculateLink(linkData);
7341
+ this.linkCandidates?.set(String(linkData.id), {
7342
+ targetId: String(this.referenceActivity.id),
7343
+ ...calculation
7344
+ });
7345
+ return calculation;
6903
7346
  });
6904
7347
  const parent = Number(this.referenceActivity.parent);
6905
7348
  if (this.parentsWithLinks.has(parent)) {
@@ -7165,6 +7608,7 @@ var ForwardPath = class extends generic_calculations_default {
7165
7608
  this.totalFloatSnltFnlt = /* @__PURE__ */ new Map();
7166
7609
  this.totalFloatForMsoMfo = /* @__PURE__ */ new Map();
7167
7610
  this.totalFloatForAsap = /* @__PURE__ */ new Map();
7611
+ this.linkCandidates = /* @__PURE__ */ new Map();
7168
7612
  this.calculationOfParentsFromLinks = /* @__PURE__ */ new Set();
7169
7613
  this.parentCalculationFromChildren = /* @__PURE__ */ new Set();
7170
7614
  this.forbiddenActivitiesLinkedToParentsWithLinks = /* @__PURE__ */ new Set();
@@ -7263,7 +7707,8 @@ var ForwardPath = class extends generic_calculations_default {
7263
7707
  constraint: constraintType,
7264
7708
  parentsWithLinks: this.pendingParentsWithLinks,
7265
7709
  parentsCalculations: this.calculationsOfParentThatImpactChildrens,
7266
- ganttInstance: this.gantt
7710
+ ganttInstance: this.gantt,
7711
+ linkCandidates: this.linkCandidates
7267
7712
  }).calculate();
7268
7713
  if (constraintType === CONSTRAINT_TYPES.ASAP) {
7269
7714
  this.totalFloatForAsap.set(activity, {
@@ -7281,7 +7726,8 @@ var ForwardPath = class extends generic_calculations_default {
7281
7726
  constraint: constraintType,
7282
7727
  parentsWithLinks: this.pendingParentsWithLinks,
7283
7728
  parentsCalculations: this.calculationsOfParentThatImpactChildrens,
7284
- ganttInstance: this.gantt
7729
+ ganttInstance: this.gantt,
7730
+ linkCandidates: this.linkCandidates
7285
7731
  }).calculate());
7286
7732
  }
7287
7733
  if (constraintType === CONSTRAINT_TYPES.ALAP) {
@@ -8318,7 +8764,6 @@ var BackwardPath = class extends generic_calculations_default {
8318
8764
  }
8319
8765
  }
8320
8766
  if ([CONSTRAINT_TYPES.SNLT].includes(activity.constraint_type)) {
8321
- const constraintDate = activity.constraint_date;
8322
8767
  const parentLinkCalcualation2 = this.calculationsOfParentThatImpactChildrens.get(parent);
8323
8768
  const lsToComparison = addDurationToDate(
8324
8769
  this.gantt.getCalendar(activity.calendar_id),
@@ -8326,6 +8771,7 @@ var BackwardPath = class extends generic_calculations_default {
8326
8771
  -activity.duration,
8327
8772
  activity
8328
8773
  );
8774
+ const constraintDate = activity.constraint_date ?? lsToComparison;
8329
8775
  ls = new Date(
8330
8776
  Math.min(constraintDate.getTime(), lsToComparison.getTime())
8331
8777
  );
@@ -8337,8 +8783,8 @@ var BackwardPath = class extends generic_calculations_default {
8337
8783
  );
8338
8784
  }
8339
8785
  if ([CONSTRAINT_TYPES.FNLT].includes(activity.constraint_type)) {
8340
- const constraintDate = activity.constraint_date;
8341
8786
  const parentLinkCalcualation2 = this.calculationsOfParentThatImpactChildrens.get(parent);
8787
+ const constraintDate = activity.constraint_date ?? parentLinkCalcualation2.lf;
8342
8788
  ls = new Date(
8343
8789
  Math.min(constraintDate.getTime(), parentLinkCalcualation2.lf.getTime())
8344
8790
  );
@@ -8722,18 +9168,20 @@ var FreeFloat = class {
8722
9168
  );
8723
9169
  }
8724
9170
  calculateFreeFloatForEndOfProject(constraint) {
9171
+ const calculation = constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap.get(this.activity.id) : this.forwardMap.get(this.activity.id);
8725
9172
  return this.parseHoursToDays(
8726
9173
  this.calculationWithCalendar(
8727
9174
  this.gantt.getSubtaskDates().end_date,
8728
- constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap.get(this.activity.id).lf : this.forwardMap.get(this.activity.id).ef
9175
+ constraint === CONSTRAINT_TYPES.ALAP ? calculation?.lf : calculation?.ef
8729
9176
  )
8730
9177
  );
8731
9178
  }
8732
9179
  calculateFreeFloatOfRestriction(constraint) {
9180
+ const calculation = this.forwardMap.get(this.activity.id);
8733
9181
  return this.parseHoursToDays(
8734
9182
  this.calculationWithCalendar(
8735
9183
  this.activity.constraint_date,
8736
- constraint === CONSTRAINT_TYPES.FNLT ? this.forwardMap.get(this.activity.id).ef : this.forwardMap.get(this.activity.id).es
9184
+ constraint === CONSTRAINT_TYPES.FNLT ? calculation?.ef : calculation?.es
8737
9185
  )
8738
9186
  );
8739
9187
  }
@@ -8751,111 +9199,33 @@ var FreeFloat = class {
8751
9199
  sucessorId,
8752
9200
  predecessorId
8753
9201
  }) {
8754
- const getPredecessorDate = ({
8755
- direction,
8756
- constraint: constraint2 = CONSTRAINT_TYPES.ASAP
8757
- }) => {
8758
- if (constraint2 === CONSTRAINT_TYPES.ALAP) {
8759
- return this.backwardMap.get(predecessorId)[direction];
9202
+ const successor = (isSuscessorAlap ? this.backwardMap : this.forwardMap).get(sucessorId);
9203
+ const predecessor = (constraint === CONSTRAINT_TYPES.ALAP ? this.backwardMap : this.forwardMap).get(predecessorId);
9204
+ const predecessorStart = constraint === CONSTRAINT_TYPES.ALAP ? predecessor?.ls : predecessor?.es;
9205
+ const predecessorFinish = constraint === CONSTRAINT_TYPES.ALAP ? predecessor?.lf : predecessor?.ef;
9206
+ return {
9207
+ fs: {
9208
+ sucessor: successor?.ls ?? successor?.es,
9209
+ predecessor: predecessorFinish
9210
+ },
9211
+ ss: {
9212
+ sucessor: successor?.ls ?? successor?.es,
9213
+ predecessor: predecessorStart
9214
+ },
9215
+ ff: {
9216
+ sucessor: successor?.lf ?? successor?.ef,
9217
+ predecessor: predecessorFinish
9218
+ },
9219
+ sf: {
9220
+ sucessor: successor?.lf ?? successor?.ef,
9221
+ predecessor: predecessorStart
8760
9222
  }
8761
- return this.forwardMap.get(predecessorId)[direction];
8762
9223
  };
8763
- if (constraint === CONSTRAINT_TYPES.ASAP || constraint === CONSTRAINT_TYPES.SNET || constraint === CONSTRAINT_TYPES.FNET || constraint === CONSTRAINT_TYPES.MSO || constraint === CONSTRAINT_TYPES.MFO) {
8764
- return {
8765
- fs: {
8766
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8767
- predecessor: getPredecessorDate({ direction: "ef" })
8768
- },
8769
- ss: {
8770
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8771
- predecessor: getPredecessorDate({ direction: "es" })
8772
- },
8773
- ff: {
8774
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8775
- predecessor: getPredecessorDate({ direction: "ef" })
8776
- },
8777
- sf: {
8778
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8779
- predecessor: getPredecessorDate({ direction: "es" })
8780
- }
8781
- };
8782
- }
8783
- if (constraint === CONSTRAINT_TYPES.ALAP) {
8784
- return {
8785
- fs: {
8786
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8787
- predecessor: getPredecessorDate({
8788
- constraint: CONSTRAINT_TYPES.ALAP,
8789
- direction: "lf"
8790
- })
8791
- },
8792
- ss: {
8793
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8794
- predecessor: getPredecessorDate({
8795
- constraint: CONSTRAINT_TYPES.ALAP,
8796
- direction: "ls"
8797
- })
8798
- },
8799
- ff: {
8800
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8801
- predecessor: getPredecessorDate({
8802
- constraint: CONSTRAINT_TYPES.ALAP,
8803
- direction: "lf"
8804
- })
8805
- },
8806
- sf: {
8807
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8808
- predecessor: getPredecessorDate({
8809
- constraint: CONSTRAINT_TYPES.ALAP,
8810
- direction: "ls"
8811
- })
8812
- }
8813
- };
8814
- }
8815
- if (constraint === CONSTRAINT_TYPES.SNLT) {
8816
- return {
8817
- fs: {
8818
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8819
- predecessor: getPredecessorDate({ direction: "ef" })
8820
- },
8821
- ss: {
8822
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8823
- predecessor: getPredecessorDate({ direction: "es" })
8824
- },
8825
- ff: {
8826
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8827
- predecessor: getPredecessorDate({ direction: "ef" })
8828
- },
8829
- sf: {
8830
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8831
- predecessor: getPredecessorDate({ direction: "es" })
8832
- }
8833
- };
8834
- }
8835
- if (constraint === CONSTRAINT_TYPES.FNLT) {
8836
- return {
8837
- fs: {
8838
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8839
- predecessor: getPredecessorDate({ direction: "ef" })
8840
- },
8841
- ss: {
8842
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).ls : this.forwardMap.get(sucessorId).es,
8843
- predecessor: getPredecessorDate({ direction: "es" })
8844
- },
8845
- ff: {
8846
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8847
- predecessor: getPredecessorDate({ direction: "ef" })
8848
- },
8849
- sf: {
8850
- sucessor: isSuscessorAlap ? this.backwardMap.get(sucessorId).lf : this.forwardMap.get(sucessorId).ef,
8851
- predecessor: getPredecessorDate({ direction: "es" })
8852
- }
8853
- };
8854
- }
8855
9224
  }
8856
9225
  calculateMsoMfo(constraint) {
8857
9226
  const CONSTRAINT_DATE2 = this.activity.constraint_date || /* @__PURE__ */ new Date();
8858
- const DATE_TO_CALCULATE = constraint === CONSTRAINT_TYPES.MSO ? this.forwardMap.get(this.activity.id).es : this.forwardMap.get(this.activity.id).ef;
9227
+ const calculation = this.forwardMap.get(this.activity.id);
9228
+ const DATE_TO_CALCULATE = constraint === CONSTRAINT_TYPES.MSO ? calculation?.es : calculation?.ef;
8859
9229
  const freeSlack = this.calculationWithCalendar(
8860
9230
  CONSTRAINT_DATE2,
8861
9231
  DATE_TO_CALCULATE
@@ -8869,6 +9239,9 @@ var FreeFloat = class {
8869
9239
  return filterValues.length ? Math.min(...filterValues) : 0;
8870
9240
  }
8871
9241
  calculationWithCalendar(dateOne, dateTwo) {
9242
+ if (!(dateOne instanceof Date) || !(dateTwo instanceof Date) || !Number.isFinite(dateOne.getTime()) || !Number.isFinite(dateTwo.getTime())) {
9243
+ return Number.NaN;
9244
+ }
8872
9245
  const key = `${dateTwo.getTime()}_${dateOne.getTime()}`;
8873
9246
  if (this.durationCache.has(key)) {
8874
9247
  return this.durationCache.get(key);
@@ -8882,7 +9255,7 @@ var FreeFloat = class {
8882
9255
  return result;
8883
9256
  }
8884
9257
  parseHoursToDays(date2) {
8885
- if (!date2) {
9258
+ if (date2 === void 0 || date2 === null) {
8886
9259
  return 0;
8887
9260
  }
8888
9261
  return date2 / this.gantt.config.hoursPerDay;
@@ -8948,6 +9321,7 @@ var CriticalPath = class {
8948
9321
  this.backwardPathCalculations = null;
8949
9322
  this.totalFloat = /* @__PURE__ */ new Map();
8950
9323
  this.freeFloat = /* @__PURE__ */ new Map();
9324
+ this.linkResidualHours = /* @__PURE__ */ new Map();
8951
9325
  this.currentCalculationId = 0;
8952
9326
  }
8953
9327
  getCurrentCalculationId = () => this.currentCalculationId;
@@ -8978,6 +9352,13 @@ var CriticalPath = class {
8978
9352
  if (!isCurrent()) return false;
8979
9353
  this.forwardPathCalculations = forwardPathCalculations;
8980
9354
  this.backwardPathCalculations = backwardPath.calculations;
9355
+ const linkResidualHours = await this.calculateLinkResidualHours(
9356
+ forwardPath.linkCandidates,
9357
+ forwardPathCalculations,
9358
+ isCurrent
9359
+ );
9360
+ if (!linkResidualHours || !isCurrent()) return false;
9361
+ this.linkResidualHours = linkResidualHours;
8981
9362
  const { totalFloatMap, freeFloatMap } = await this.calculateFloatsAsync(
8982
9363
  forwardPath,
8983
9364
  backwardPath,
@@ -8997,6 +9378,38 @@ var CriticalPath = class {
8997
9378
  throw error;
8998
9379
  }
8999
9380
  }
9381
+ async calculateLinkResidualHours(linkCandidates, forwardCalculations, isCurrent = () => true) {
9382
+ const residuals = /* @__PURE__ */ new Map();
9383
+ let sliceStartTime = performance.now();
9384
+ for (const [linkId, candidate] of linkCandidates) {
9385
+ sliceStartTime = await yieldIfNeeded2(sliceStartTime);
9386
+ if (!isCurrent()) return null;
9387
+ const targetId = Number(candidate.targetId);
9388
+ const target = this.gantt.getTask(targetId);
9389
+ const settled = forwardCalculations.get(targetId);
9390
+ const candidateEf = candidate.ef;
9391
+ const settledEf = settled?.ef;
9392
+ const calendar = target && this.gantt.getCalendar(target.calendar_id);
9393
+ if (!calendar || !(candidateEf instanceof Date) || !(settledEf instanceof Date) || !Number.isFinite(candidateEf.getTime()) || !Number.isFinite(settledEf.getTime())) {
9394
+ residuals.set(linkId, null);
9395
+ continue;
9396
+ }
9397
+ const candidateTime = candidateEf.getTime();
9398
+ const settledTime = settledEf.getTime();
9399
+ const duration = settledTime >= candidateTime ? calendar.calculateDuration({
9400
+ start_date: candidateEf,
9401
+ end_date: settledEf
9402
+ }) : -calendar.calculateDuration({
9403
+ start_date: settledEf,
9404
+ end_date: candidateEf
9405
+ });
9406
+ residuals.set(
9407
+ linkId,
9408
+ Number.isFinite(duration) ? Object.is(duration, -0) ? 0 : duration : null
9409
+ );
9410
+ }
9411
+ return residuals;
9412
+ }
9000
9413
  async calculateFloatsAsync(forward, backward, isCurrent) {
9001
9414
  try {
9002
9415
  const projectDates = this.gantt.getSubtaskDates();
@@ -9100,14 +9513,52 @@ function computeLevels(state) {
9100
9513
  for (const id of activitiesById.keys()) resolveLevel(id, /* @__PURE__ */ new Set());
9101
9514
  return levelById;
9102
9515
  }
9103
- function toLegacyTasks(state) {
9516
+ function createLegacyIds(state) {
9517
+ const canonicalIds = /* @__PURE__ */ new Set();
9518
+ state.forEachActivity((activity, id) => {
9519
+ canonicalIds.add(String(id));
9520
+ if (activity.parentId !== null) canonicalIds.add(String(activity.parentId));
9521
+ });
9522
+ const numericCounts = /* @__PURE__ */ new Map();
9523
+ for (const id of canonicalIds) {
9524
+ const numeric = Number(id);
9525
+ if (Number.isSafeInteger(numeric) && numeric !== 0) {
9526
+ numericCounts.set(numeric, (numericCounts.get(numeric) ?? 0) + 1);
9527
+ }
9528
+ }
9529
+ const canonicalToLegacy = /* @__PURE__ */ new Map();
9530
+ const legacyToCanonical = /* @__PURE__ */ new Map();
9531
+ for (const id of canonicalIds) {
9532
+ const numeric = Number(id);
9533
+ if (Number.isSafeInteger(numeric) && numeric !== 0 && numericCounts.get(numeric) === 1) {
9534
+ canonicalToLegacy.set(id, numeric);
9535
+ legacyToCanonical.set(numeric, id);
9536
+ }
9537
+ }
9538
+ let next = 1;
9539
+ const toLegacy = (rawId) => {
9540
+ const id = String(rawId);
9541
+ const existing = canonicalToLegacy.get(id);
9542
+ if (existing !== void 0) return existing;
9543
+ while (legacyToCanonical.has(next)) next += 1;
9544
+ canonicalToLegacy.set(id, next);
9545
+ legacyToCanonical.set(next, id);
9546
+ return next++;
9547
+ };
9548
+ for (const id of canonicalIds) toLegacy(id);
9549
+ return {
9550
+ toLegacy,
9551
+ toCanonical: (id) => typeof id === "number" ? legacyToCanonical.get(id) ?? null : canonicalToLegacy.has(String(id)) ? String(id) : null
9552
+ };
9553
+ }
9554
+ function toLegacyTasks(state, ids) {
9104
9555
  const levelById = computeLevels(state);
9105
9556
  const leveled = /* @__PURE__ */ new Map();
9106
9557
  state.forEachActivity((activity, id) => {
9107
9558
  const key = String(id);
9108
9559
  leveled.set(key, {
9109
- id: Number(id),
9110
- parent: activity.parentId === null ? 0 : Number(activity.parentId),
9560
+ id: ids.toLegacy(key),
9561
+ parent: activity.parentId === null ? 0 : ids.toLegacy(activity.parentId),
9111
9562
  $level: levelById.get(key) ?? 0,
9112
9563
  $source: state.getOutgoingLinkIds(key),
9113
9564
  $target: state.getIncomingLinkIds(key),
@@ -9181,7 +9632,8 @@ async function createGanttAdapter(state, hoursPerDay) {
9181
9632
  };
9182
9633
  const bounds = projectBounds(state);
9183
9634
  await yieldToBrowser();
9184
- const leveled = toLegacyTasks(state);
9635
+ const ids = createLegacyIds(state);
9636
+ const leveled = toLegacyTasks(state, ids);
9185
9637
  await yieldToBrowser();
9186
9638
  const byTime = [...leveled.values()].sort(
9187
9639
  (a, b) => (a.start_date?.getTime() ?? 0) - (b.start_date?.getTime() ?? 0)
@@ -9191,24 +9643,31 @@ async function createGanttAdapter(state, hoursPerDay) {
9191
9643
  return {
9192
9644
  fullyParsed: true,
9193
9645
  config: { hoursPerDay },
9194
- getTask: (id) => leveled.get(String(id)) ?? null,
9646
+ getTask: (id) => {
9647
+ const canonicalId = ids.toCanonical(id);
9648
+ return canonicalId === null ? null : leveled.get(canonicalId) ?? null;
9649
+ },
9195
9650
  getLink: (id) => {
9196
9651
  const link = state.getLink(id);
9197
9652
  if (!link) return null;
9198
9653
  return {
9199
9654
  ...link,
9200
- source: Number(link.source),
9201
- target: Number(link.target),
9655
+ source: ids.toLegacy(link.source),
9656
+ target: ids.toLegacy(link.target),
9202
9657
  lag: link.lag
9203
9658
  };
9204
9659
  },
9205
9660
  getCalendar: resolveCalendar,
9206
- getChildren: (parentId) => state.getChildren(parentId).map((childId) => Number(childId)),
9661
+ getChildren: (parentId) => {
9662
+ const canonicalId = ids.toCanonical(parentId);
9663
+ return canonicalId === null ? [] : state.getChildren(canonicalId).map(ids.toLegacy);
9664
+ },
9207
9665
  eachTask: (cb) => {
9208
9666
  for (const task of treeOrdered) cb(task);
9209
9667
  },
9210
9668
  getTaskByTime: () => byTime,
9211
9669
  getSubtaskDates: () => bounds,
9670
+ getCanonicalActivityId: ids.toCanonical,
9212
9671
  refreshData: () => {
9213
9672
  }
9214
9673
  };
@@ -9216,21 +9675,92 @@ async function createGanttAdapter(state, hoursPerDay) {
9216
9675
 
9217
9676
  // src/critical-path/run-critical-path.ts
9218
9677
  var CP_WRITE_YIELD_EVERY = 1e3;
9219
- function fieldsForActivity(activityId, progress, maps) {
9220
- const toDate = (value) => value == null ? null : new Date(value);
9221
- const forward = maps.forward?.get(activityId);
9222
- const backward = maps.backward?.get(activityId);
9223
- const totalSlack = maps.totalFloat.get(activityId) ?? null;
9678
+ function fieldsForLink(rawResidual, sourceIsCritical, targetIsCritical, targetProgress) {
9679
+ const freeSlackHours = typeof rawResidual === "number" && Number.isFinite(rawResidual) ? Object.is(rawResidual, -0) ? 0 : rawResidual : null;
9224
9680
  return {
9225
- criticalPath: {
9226
- earlyStart: toDate(forward?.es),
9681
+ freeSlackHours,
9682
+ isCritical: freeSlackHours !== null && freeSlackHours <= 0 && sourceIsCritical && targetIsCritical && targetProgress === 0
9683
+ };
9684
+ }
9685
+ async function collectActivityFields(state, maps, hoursPerDay, toCanonicalActivityId, isCurrent) {
9686
+ const fields = /* @__PURE__ */ new Map();
9687
+ let sinceYield = 0;
9688
+ for (const calculationId of maps.forward?.keys() ?? []) {
9689
+ if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9690
+ sinceYield = 0;
9691
+ await yieldToBrowser();
9692
+ if (!isCurrent()) return null;
9693
+ }
9694
+ const activityId = toCanonicalActivityId(calculationId);
9695
+ if (activityId === null) continue;
9696
+ const activity = state.getActivity(activityId);
9697
+ if (!activity) continue;
9698
+ fields.set(
9699
+ String(activityId),
9700
+ fieldsForActivity(
9701
+ calculationId,
9702
+ Number(activity.progress ?? 0),
9703
+ maps,
9704
+ hoursPerDay
9705
+ )
9706
+ );
9707
+ }
9708
+ return isCurrent() ? fields : null;
9709
+ }
9710
+ async function collectLinkFields(state, linkResidualHours, activityFields, isCurrent) {
9711
+ const fields = /* @__PURE__ */ new Map();
9712
+ let sinceYield = 0;
9713
+ for (const link of state.getAllLinks()) {
9714
+ if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9715
+ sinceYield = 0;
9716
+ await yieldToBrowser();
9717
+ if (!isCurrent()) return null;
9718
+ }
9719
+ fields.set(
9720
+ String(link.id),
9721
+ fieldsForLink(
9722
+ linkResidualHours.get(String(link.id)),
9723
+ activityFields.get(link.source)?.isCritical === true,
9724
+ activityFields.get(link.target)?.isCritical === true,
9725
+ state.getActivity(link.target)?.progress
9726
+ )
9727
+ );
9728
+ }
9729
+ return isCurrent() ? fields : null;
9730
+ }
9731
+ var slackDaysToHours = (valueDays, hoursPerDay) => {
9732
+ if (typeof valueDays !== "number" || !Number.isFinite(valueDays) || !Number.isFinite(hoursPerDay) || hoursPerDay <= 0) {
9733
+ return null;
9734
+ }
9735
+ const hours = valueDays * hoursPerDay;
9736
+ if (!Number.isFinite(hours)) return null;
9737
+ return Object.is(hours, -0) ? 0 : hours;
9738
+ };
9739
+ function fieldsForActivity(activityId, progress, maps, hoursPerDay) {
9740
+ const toDate = (value) => {
9741
+ if (value == null) return null;
9742
+ const date2 = new Date(value);
9743
+ return Number.isFinite(date2.getTime()) ? date2 : null;
9744
+ };
9745
+ const forward = maps.forward?.get(activityId);
9746
+ const backward = maps.backward?.get(activityId);
9747
+ const totalSlackHours = slackDaysToHours(
9748
+ maps.totalFloat.get(activityId),
9749
+ hoursPerDay
9750
+ );
9751
+ return {
9752
+ criticalPath: {
9753
+ earlyStart: toDate(forward?.es),
9227
9754
  earlyFinish: toDate(forward?.ef),
9228
9755
  lateStart: toDate(backward?.ls),
9229
9756
  lateFinish: toDate(backward?.lf),
9230
- totalSlackHours: totalSlack
9757
+ totalSlackHours
9231
9758
  },
9232
- isCritical: progress < 100 && totalSlack != null && totalSlack <= 0,
9233
- freeSlackHours: maps.freeFloat.get(activityId)?.activityFreeFloat ?? null
9759
+ isCritical: progress < 100 && totalSlackHours !== null && totalSlackHours <= 0,
9760
+ freeSlackHours: slackDaysToHours(
9761
+ maps.freeFloat.get(activityId)?.activityFreeFloat,
9762
+ hoursPerDay
9763
+ )
9234
9764
  };
9235
9765
  }
9236
9766
  var datesEqual = (left, right) => left === right || left !== null && right !== null && left.getTime() === right.getTime();
@@ -9243,6 +9773,14 @@ function setCriticalPathFieldsIfChanged(state, activityId, fields) {
9243
9773
  }
9244
9774
  state.setActivityFields(activityId, fields);
9245
9775
  }
9776
+ function setCriticalPathLinkFieldsIfChanged(state, linkId, fields) {
9777
+ const link = state.getLink(linkId);
9778
+ if (!link || Object.is(link.freeSlackHours, fields.freeSlackHours) && Object.is(link.isCritical, fields.isCritical)) {
9779
+ return;
9780
+ }
9781
+ state.setLinkField(linkId, "freeSlackHours", fields.freeSlackHours);
9782
+ state.setLinkField(linkId, "isCritical", fields.isCritical);
9783
+ }
9246
9784
  async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => true) {
9247
9785
  if (!isCurrent()) return null;
9248
9786
  const gantt = await createGanttAdapter(state, hoursPerDay);
@@ -9260,61 +9798,36 @@ async function computeCriticalPathFields(state, hoursPerDay, isCurrent = () => t
9260
9798
  totalFloat: criticalPath.totalFloat,
9261
9799
  freeFloat: criticalPath.freeFloat
9262
9800
  };
9263
- const activityIds = /* @__PURE__ */ new Set([...maps.forward?.keys() ?? []]);
9264
- const fieldsByActivity = /* @__PURE__ */ new Map();
9265
- let sinceYield = 0;
9266
- for (const activityId of activityIds) {
9267
- if (++sinceYield >= CP_WRITE_YIELD_EVERY) {
9268
- sinceYield = 0;
9269
- await yieldToBrowser();
9270
- if (!isCurrent()) return null;
9271
- }
9272
- const activity = state.getActivity(activityId);
9273
- if (!activity) continue;
9274
- const progress = Number(activity.progress ?? 0);
9275
- fieldsByActivity.set(
9276
- activityId,
9277
- fieldsForActivity(activityId, progress, maps)
9278
- );
9279
- }
9280
- return isCurrent() ? fieldsByActivity : null;
9281
- }
9282
- async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9283
- const fieldsByActivity = await computeCriticalPathFields(
9801
+ const activities = await collectActivityFields(
9284
9802
  state,
9803
+ maps,
9285
9804
  hoursPerDay,
9805
+ gantt.getCanonicalActivityId,
9806
+ isCurrent
9807
+ );
9808
+ if (!activities) return null;
9809
+ const links = await collectLinkFields(
9810
+ state,
9811
+ criticalPath.linkResidualHours,
9812
+ activities,
9286
9813
  isCurrent
9287
9814
  );
9288
- if (!fieldsByActivity || !isCurrent()) return false;
9289
- for (const [activityId, fields] of fieldsByActivity) {
9815
+ return links ? { activities, links } : null;
9816
+ }
9817
+ async function applyCriticalPath(state, hoursPerDay, isCurrent = () => true) {
9818
+ const fields = await computeCriticalPathFields(state, hoursPerDay, isCurrent);
9819
+ if (!fields || !isCurrent()) return false;
9820
+ for (const [activityId, activityFields] of fields.activities) {
9290
9821
  if (!isCurrent()) return false;
9291
- setCriticalPathFieldsIfChanged(state, activityId, fields);
9822
+ setCriticalPathFieldsIfChanged(state, activityId, activityFields);
9823
+ }
9824
+ for (const [linkId, linkFields] of fields.links) {
9825
+ if (!isCurrent()) return false;
9826
+ setCriticalPathLinkFieldsIfChanged(state, linkId, linkFields);
9292
9827
  }
9293
9828
  return true;
9294
9829
  }
9295
9830
 
9296
- // src/constants/activity-properties.ts
9297
- var ACTIVITY_PROPERTY = {
9298
- PARENT: "parentId",
9299
- TYPE: "type",
9300
- START_DATE: "startDate",
9301
- END_DATE: "endDate",
9302
- CONSTRAINT_TYPE: "constraintType",
9303
- CONSTRAINT_DATE: "constraintDate",
9304
- PROGRESS: "progress",
9305
- COST: "cost",
9306
- USED_COST: "usedCost",
9307
- REAL_COST: "realCost",
9308
- WORK_HOURS: "workHours",
9309
- REAL_WORK_HOURS: "realWorkHours",
9310
- CORRELATIVE_ID: "correlativeId",
9311
- CUSTOM_ID: "customId",
9312
- NEW_ACTIVITIES_ARRAY: "newActivityIds",
9313
- HAS_NEW_ACTIVITIES: "hasNewActivities"};
9314
- var LINK_PROPERTY = {
9315
- TYPE: "type",
9316
- LAG: "lag"};
9317
-
9318
9831
  // src/propagations/upward/real-cost.ts
9319
9832
  function emitRealCost(state) {
9320
9833
  const rootIds = [];
@@ -9367,6 +9880,8 @@ var NON_SCHEDULING_INLINE_COLUMNS = /* @__PURE__ */ new Set([
9367
9880
  COLUMN.TAGS
9368
9881
  ]);
9369
9882
  var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9883
+ "activity-clipboard-clear",
9884
+ "activity-copy",
9370
9885
  "selection-toggle",
9371
9886
  "selection-replace",
9372
9887
  "visibility-set",
@@ -9380,6 +9895,8 @@ var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
9380
9895
  "status-criteria-set"
9381
9896
  ]);
9382
9897
  var PURE_VIEW_STATE_KINDS = /* @__PURE__ */ new Set([
9898
+ "activity-clipboard-clear",
9899
+ "activity-copy",
9383
9900
  "selection-toggle",
9384
9901
  "selection-replace",
9385
9902
  "visibility-set",
@@ -9408,9 +9925,15 @@ function isSchedulingDispatch(action, structuralStaleKinds) {
9408
9925
  return true;
9409
9926
  }
9410
9927
  function dispatchChangesSchedulingState(action) {
9928
+ if (action.kind === "activity-lookahead-sync") {
9929
+ return action.persistedProgress !== void 0;
9930
+ }
9411
9931
  return isSchedulingDispatch(action, NO_STRUCTURAL_EXEMPTIONS);
9412
9932
  }
9413
9933
  function resolveRunCriticalPath(action) {
9934
+ if (action.kind === "activity-lookahead-sync") {
9935
+ return action.persistedProgress !== void 0;
9936
+ }
9414
9937
  return isSchedulingDispatch(
9415
9938
  action,
9416
9939
  STRUCTURAL_KINDS_LEAVING_CRITICAL_PATH_STALE
@@ -9696,6 +10219,23 @@ function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
9696
10219
  return effects;
9697
10220
  }
9698
10221
 
10222
+ // src/dispatch/conversion.ts
10223
+ function snapshotToLink(l) {
10224
+ const view = {
10225
+ id: String(l.id),
10226
+ source: String(l.source),
10227
+ target: String(l.target),
10228
+ type: l.type,
10229
+ lag: Number(l.lag),
10230
+ freeSlackHours: typeof l.freeSlackHours === "number" && Number.isFinite(l.freeSlackHours) ? l.freeSlackHours : null,
10231
+ isCritical: l.isCritical === true
10232
+ };
10233
+ if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
10234
+ if (l.ganttId != null) view.ganttId = Number(l.ganttId);
10235
+ if (l.sectorId != null) view.sectorId = Number(l.sectorId);
10236
+ return view;
10237
+ }
10238
+
9699
10239
  // src/dispatch/shared/change-set.ts
9700
10240
  async function assembleChangeSet(adapter, args) {
9701
10241
  const allTouched = collectChangeSetIds(adapter, args);
@@ -9705,7 +10245,8 @@ async function assembleChangeSet(adapter, args) {
9705
10245
  const insertedIds = collectInsertedIds(captured);
9706
10246
  mergeDirtyBeforeImages(adapter, captured, insertedIds, before, allTouched);
9707
10247
  }
9708
- const beforeDiffEffects = args.sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
10248
+ const sirDetection = args.sirDetection;
10249
+ const beforeDiffEffects = sirDetection === "before-diff" ? detectSirAutoReject(before, adapter, allTouched) : [];
9709
10250
  const correlativeBefore = buildCorrelativeBeforeMap(
9710
10251
  args.correlativeShifts,
9711
10252
  allTouched
@@ -9716,18 +10257,91 @@ async function assembleChangeSet(adapter, args) {
9716
10257
  allTouched,
9717
10258
  correlativeBefore
9718
10259
  );
9719
- const effects = args.sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
10260
+ const effects = sirDetection === "after-diff" ? detectSirAutoReject(before, adapter, allTouched) : beforeDiffEffects;
9720
10261
  const warnings = args.warnings ?? [];
10262
+ const linkChanges = mergeLinkChanges(
10263
+ args.links ?? [],
10264
+ captured ? buildCapturedCriticalPathLinkChanges(adapter, captured) : []
10265
+ );
9721
10266
  return {
9722
10267
  source: args.source,
9723
10268
  activities: activityChanges,
9724
- links: args.links ?? [],
10269
+ links: linkChanges,
9725
10270
  calendars: [],
9726
10271
  trackingEvents: args.trackingEvents ?? [],
9727
10272
  ...effects.length > 0 ? { effects } : {},
9728
10273
  ...warnings.length > 0 ? { warnings } : {}
9729
10274
  };
9730
10275
  }
10276
+ function buildCapturedCriticalPathLinkChanges(adapter, captured) {
10277
+ const changes = [];
10278
+ for (const operation of captured.ops) {
10279
+ if (operation.kind !== "link-field") continue;
10280
+ const after = adapter.getLink(operation.linkId);
10281
+ if (!after) continue;
10282
+ const fields = {};
10283
+ for (const field of ["freeSlackHours", "isCritical"]) {
10284
+ if (!Object.is(operation.before[field], after[field])) {
10285
+ fields[field] = {
10286
+ before: operation.before[field],
10287
+ after: after[field]
10288
+ };
10289
+ }
10290
+ }
10291
+ if (Object.keys(fields).length === 0) continue;
10292
+ changes.push({
10293
+ id: operation.linkId,
10294
+ kind: "updated",
10295
+ fields,
10296
+ after: snapshotToLink(after)
10297
+ });
10298
+ }
10299
+ return changes;
10300
+ }
10301
+ function mergeLinkChanges(explicit, captured) {
10302
+ const merged = [];
10303
+ const indexById = /* @__PURE__ */ new Map();
10304
+ for (const change of explicit) {
10305
+ const index = indexById.get(change.id);
10306
+ if (index === void 0) {
10307
+ indexById.set(change.id, merged.length);
10308
+ merged.push(change);
10309
+ continue;
10310
+ }
10311
+ const current = merged[index];
10312
+ if ((current.kind === "updated" || current.kind === "created") && change.kind === "updated") {
10313
+ merged[index] = {
10314
+ ...current,
10315
+ fields: { ...current.fields, ...change.fields },
10316
+ after: change.after
10317
+ };
10318
+ continue;
10319
+ }
10320
+ merged[index] = change;
10321
+ }
10322
+ for (const change of captured) {
10323
+ const index = indexById.get(change.id);
10324
+ if (index === void 0) {
10325
+ indexById.set(change.id, merged.length);
10326
+ merged.push(change);
10327
+ continue;
10328
+ }
10329
+ const current = merged[index];
10330
+ if (current.kind === "deleted") continue;
10331
+ const capturedFields = current.kind === "created" ? Object.fromEntries(
10332
+ Object.entries(change.fields ?? {}).map(([field, value]) => [
10333
+ field,
10334
+ { before: void 0, after: value.after }
10335
+ ])
10336
+ ) : change.fields;
10337
+ merged[index] = {
10338
+ ...current,
10339
+ fields: { ...current.fields, ...capturedFields },
10340
+ after: change.after
10341
+ };
10342
+ }
10343
+ return merged;
10344
+ }
9731
10345
  function buildCorrelativeBeforeMap(shifts, touched) {
9732
10346
  if (!shifts || shifts.length === 0) return void 0;
9733
10347
  const correlativeBefore = /* @__PURE__ */ new Map();
@@ -9763,21 +10377,6 @@ function collectInsertedIds(captured) {
9763
10377
  );
9764
10378
  }
9765
10379
 
9766
- // src/dispatch/conversion.ts
9767
- function snapshotToLink(l) {
9768
- const view = {
9769
- id: String(l.id),
9770
- source: String(l.source),
9771
- target: String(l.target),
9772
- type: l.type,
9773
- lag: Number(l.lag)
9774
- };
9775
- if (l.proplannerId != null) view.proplannerId = Number(l.proplannerId);
9776
- if (l.ganttId != null) view.ganttId = Number(l.ganttId);
9777
- if (l.sectorId != null) view.sectorId = Number(l.sectorId);
9778
- return view;
9779
- }
9780
-
9781
10380
  // src/dispatch/shared/helpers.ts
9782
10381
  function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
9783
10382
  const out = [];
@@ -10122,6 +10721,26 @@ function collectDeterministicIds(operations) {
10122
10721
  });
10123
10722
  return ids.size > 0 ? ids : void 0;
10124
10723
  }
10724
+ function resolveDeterministicIds(deterministicIds, deps) {
10725
+ if (deterministicIds === void 0) return void 0;
10726
+ const resolved = /* @__PURE__ */ new Map();
10727
+ const assigned = /* @__PURE__ */ new Set();
10728
+ const nextAvailableId = () => {
10729
+ let candidate = deps.linkIdGen.next();
10730
+ while (deps.adapter.getLink(candidate) !== null || deps.isLinkIdReserved(String(candidate)) || assigned.has(String(candidate))) {
10731
+ candidate = deps.linkIdGen.next();
10732
+ }
10733
+ return candidate;
10734
+ };
10735
+ for (const [operationIndex, explicitId] of deterministicIds) {
10736
+ const isReserved = deps.adapter.getLink(explicitId) !== null || deps.isLinkIdReserved(String(explicitId)) || assigned.has(String(explicitId));
10737
+ const linkId = isReserved ? nextAvailableId() : explicitId;
10738
+ if (!isReserved) deps.linkIdGen.bump(explicitId);
10739
+ assigned.add(String(linkId));
10740
+ resolved.set(operationIndex, linkId);
10741
+ }
10742
+ return resolved;
10743
+ }
10125
10744
  function batchOpToLinkOperation(op, hoursPerDay) {
10126
10745
  if (op.kind === "create") {
10127
10746
  return {
@@ -10154,7 +10773,7 @@ async function dispatchLinkBatch(operations, source, options, deps, deterministi
10154
10773
  operations,
10155
10774
  adapter,
10156
10775
  linkIdGen,
10157
- deterministicIds
10776
+ resolveDeterministicIds(deterministicIds, deps)
10158
10777
  );
10159
10778
  if (operations.length === 1 && applied[0] && applied[0].rejected !== null && source.kind !== "inline-edit") {
10160
10779
  return { ok: false, reason: applied[0].rejected };
@@ -10251,7 +10870,7 @@ async function dispatchInlineEditLinks(action, options, deps) {
10251
10870
  const { specs: specsInDays } = parsePredecessorString(action.newValue);
10252
10871
  const specs = specsInDays.map((spec) => ({
10253
10872
  ...spec,
10254
- lag: lagDaysToHours(spec.lag, sector.hoursPerDay)
10873
+ lag: lagDaysToHours(roundLagDays(spec.lag), sector.hoursPerDay)
10255
10874
  }));
10256
10875
  const byCorrelative = /* @__PURE__ */ new Map();
10257
10876
  adapter.forEachActivity((a) => {
@@ -10336,26 +10955,19 @@ function recomputeHhCascadeForParent(startParentId, adapter) {
10336
10955
  if (!startParentId || startParentId === "0") return;
10337
10956
  let currentParentId = String(startParentId);
10338
10957
  while (currentParentId !== null) {
10339
- const childIds = adapter.getChildren(currentParentId);
10340
- let hhSum = 0;
10341
- let rwSum = 0;
10342
- for (const childId of childIds) {
10343
- const child = adapter.getActivity(childId);
10344
- if (!child) continue;
10345
- const hh = readNumber(child, ACTIVITY_PROPERTY.WORK_HOURS);
10346
- const rw = readChildRealWork(childId, child, hh, adapter);
10347
- hhSum += hh;
10348
- rwSum += rw;
10349
- }
10958
+ const { workHours, realWorkHours } = computeParentHhRollup(
10959
+ currentParentId,
10960
+ adapter
10961
+ );
10350
10962
  adapter.setActivityField(
10351
10963
  currentParentId,
10352
10964
  ACTIVITY_PROPERTY.WORK_HOURS,
10353
- hhSum
10965
+ workHours
10354
10966
  );
10355
10967
  adapter.setActivityField(
10356
10968
  currentParentId,
10357
10969
  ACTIVITY_PROPERTY.REAL_WORK_HOURS,
10358
- rwSum
10970
+ realWorkHours
10359
10971
  );
10360
10972
  const parentSnap = adapter.getActivity(currentParentId);
10361
10973
  if (!parentSnap) break;
@@ -10371,6 +10983,40 @@ function recomputeHhCascadeForActivity(activityId, adapter) {
10371
10983
  if (!parentId || isRootParent(parentId)) return;
10372
10984
  recomputeHhCascadeForParent(parentId, adapter);
10373
10985
  }
10986
+ function recomputeRealWorkFromProgress(progressIds, parentIdsDeepestFirst, adapter) {
10987
+ for (const activityId of progressIds) {
10988
+ if (adapter.hasChildren(activityId)) continue;
10989
+ const activity = adapter.getActivity(activityId);
10990
+ if (!activity) continue;
10991
+ adapter.setActivityField(
10992
+ activityId,
10993
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
10994
+ computeLeafRealWork(
10995
+ readNumber(activity, ACTIVITY_PROPERTY.WORK_HOURS),
10996
+ readNumber(activity, ACTIVITY_PROPERTY.PROGRESS)
10997
+ )
10998
+ );
10999
+ }
11000
+ for (const parentId of parentIdsDeepestFirst) {
11001
+ adapter.setActivityField(
11002
+ parentId,
11003
+ ACTIVITY_PROPERTY.REAL_WORK_HOURS,
11004
+ computeParentHhRollup(parentId, adapter).realWorkHours
11005
+ );
11006
+ }
11007
+ }
11008
+ function computeParentHhRollup(parentId, adapter) {
11009
+ let workHours = 0;
11010
+ let realWorkHours = 0;
11011
+ for (const childId of adapter.getChildren(parentId)) {
11012
+ const child = adapter.getActivity(childId);
11013
+ if (!child) continue;
11014
+ const childWorkHours = readNumber(child, ACTIVITY_PROPERTY.WORK_HOURS);
11015
+ workHours += childWorkHours;
11016
+ realWorkHours += readChildRealWork(childId, child, childWorkHours, adapter);
11017
+ }
11018
+ return { workHours, realWorkHours };
11019
+ }
10374
11020
  function readNumber(activity, key) {
10375
11021
  const v = activity[key];
10376
11022
  if (typeof v === "number" && Number.isFinite(v)) return v;
@@ -10588,6 +11234,11 @@ function buildNewActivity(input, adapter) {
10588
11234
  ...base,
10589
11235
  startDate,
10590
11236
  endDate,
11237
+ calendarDuration: computeCalendarDuration({
11238
+ type: base.type,
11239
+ startDate,
11240
+ endDate
11241
+ }),
10591
11242
  newActivityIds: [...base.newActivityIds],
10592
11243
  pendingRequestIds: [...base.pendingRequestIds],
10593
11244
  responsableIds: [...base.responsableIds],
@@ -10738,6 +11389,36 @@ function computeFractionalCidAtIndex(adapter, parentKey, targetIndex, excludeId)
10738
11389
  return (before + after) / 2;
10739
11390
  }
10740
11391
 
11392
+ // src/dispatch/creation-overrides.ts
11393
+ var PUBLIC_CREATION_OVERRIDE_FIELDS = /* @__PURE__ */ new Set([
11394
+ ...[...CREATION_OVERRIDE_FIELDS].filter((field) => field !== "durationHours"),
11395
+ "durationDays"
11396
+ ]);
11397
+ function normalizeActivityCreationOverrides(raw, hoursPerDay) {
11398
+ if (raw === void 0) return { ok: true, value: {} };
11399
+ const prototype = Object.getPrototypeOf(raw);
11400
+ if (prototype !== null && prototype !== Object.prototype) {
11401
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11402
+ }
11403
+ for (const key of Reflect.ownKeys(raw)) {
11404
+ if (typeof key === "symbol" || !PUBLIC_CREATION_OVERRIDE_FIELDS.has(key)) {
11405
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11406
+ }
11407
+ }
11408
+ const { durationDays, ...rest } = raw;
11409
+ if (durationDays !== void 0 && (!Number.isFinite(durationDays) || durationDays < 0)) {
11410
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
11411
+ }
11412
+ return {
11413
+ ok: true,
11414
+ value: {
11415
+ ...rest,
11416
+ ...durationDays === 0 && (rest.type === void 0 || rest.type === "task") ? { type: "milestone" } : {},
11417
+ ...durationDays === void 0 ? {} : { durationHours: durationToHours(durationDays, hoursPerDay) }
11418
+ }
11419
+ };
11420
+ }
11421
+
10741
11422
  // src/dispatch/handlers/activity-create.ts
10742
11423
  async function createActivityCore(action, deps, opts = {}) {
10743
11424
  const { adapter, scheduler, sector, calendars, activityIdGen, uidGen } = deps;
@@ -10745,7 +11426,12 @@ async function createActivityCore(action, deps, opts = {}) {
10745
11426
  const target = validateCreateTarget(action, adapter);
10746
11427
  if (!target.ok) return target;
10747
11428
  const { parent, anchorSiblingId, isRoot } = target;
10748
- const newId = resolveNewActivityId(action.activityId, adapter, activityIdGen);
11429
+ const newId = resolveNewActivityId(
11430
+ action.activityId,
11431
+ adapter,
11432
+ activityIdGen,
11433
+ deps.isActivityIdReserved
11434
+ );
10749
11435
  const newUid = uidGen.next();
10750
11436
  const activityInput = {
10751
11437
  id: newId,
@@ -10838,9 +11524,18 @@ function validateAnchorSibling(action, adapter, isRoot, anchorSiblingId) {
10838
11524
  }
10839
11525
  return null;
10840
11526
  }
10841
- function resolveNewActivityId(explicitId, adapter, activityIdGen) {
10842
- if (explicitId === void 0) return activityIdGen.next();
10843
- if (adapter.activityExists(explicitId)) return activityIdGen.next();
11527
+ function resolveNewActivityId(explicitId, adapter, activityIdGen, isActivityIdReserved) {
11528
+ const nextAvailableId = () => {
11529
+ let candidate = activityIdGen.next();
11530
+ while (adapter.activityExists(candidate) || isActivityIdReserved(String(candidate))) {
11531
+ candidate = activityIdGen.next();
11532
+ }
11533
+ return candidate;
11534
+ };
11535
+ if (explicitId === void 0) return nextAvailableId();
11536
+ if (adapter.activityExists(explicitId) || isActivityIdReserved(String(explicitId))) {
11537
+ return nextAvailableId();
11538
+ }
10844
11539
  activityIdGen.bump(explicitId);
10845
11540
  return explicitId;
10846
11541
  }
@@ -10919,7 +11614,15 @@ async function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, h
10919
11614
  }
10920
11615
  async function dispatchActivityCreate(action, options, deps) {
10921
11616
  const { adapter, scheduler, sector } = deps;
10922
- const core = await createActivityCore(action, deps);
11617
+ const normalized2 = normalizeActivityCreationOverrides(
11618
+ action.overrides,
11619
+ sector.hoursPerDay
11620
+ );
11621
+ if (!normalized2.ok) return normalized2;
11622
+ const core = await createActivityCore(
11623
+ { ...action, overrides: normalized2.value },
11624
+ deps
11625
+ );
10923
11626
  if (!core.ok) return core;
10924
11627
  const { newId } = core;
10925
11628
  const createdAutoScheduling = core.newActivity.autoScheduling;
@@ -11131,7 +11834,7 @@ async function dispatchActivityDelete(action, options, deps) {
11131
11834
  for (const linkId of incidentLinkIds) {
11132
11835
  adapter.removeLink(linkId);
11133
11836
  }
11134
- const viewStateChanges = captureDeletedViewState(adapter, toDelete);
11837
+ const viewStateChanges2 = captureDeletedViewState(adapter, toDelete);
11135
11838
  let removed = 0;
11136
11839
  for (const id of toDelete) {
11137
11840
  removed += 1;
@@ -11220,7 +11923,7 @@ async function dispatchActivityDelete(action, options, deps) {
11220
11923
  });
11221
11924
  return {
11222
11925
  ok: true,
11223
- changes: viewStateChanges.length > 0 ? { ...changes, viewState: viewStateChanges } : changes,
11926
+ changes: viewStateChanges2.length > 0 ? { ...changes, viewState: viewStateChanges2 } : changes,
11224
11927
  __beforeSnap: deletedRowsSnap,
11225
11928
  __beforeLinks: beforeLinksByStringId
11226
11929
  };
@@ -11328,7 +12031,7 @@ async function dispatchActivityBatch(action, options, deps) {
11328
12031
  source: link.source,
11329
12032
  target: link.target,
11330
12033
  type: link.type,
11331
- lag: options.inputUnit === "hours" ? link.lag : lagDaysToHours(link.lag, deps.sector.hoursPerDay)
12034
+ lag: lagDaysToHours(link.lag, deps.sector.hoursPerDay)
11332
12035
  },
11333
12036
  linkId: link.linkId
11334
12037
  }));
@@ -11400,7 +12103,7 @@ function validateBatch(action, deps) {
11400
12103
  }
11401
12104
  if (item.activityId !== void 0) {
11402
12105
  const activityId = String(item.activityId);
11403
- if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId) || action.mode === "append" && deps.adapter.getActivity(activityId) !== null) {
12106
+ if (activityId.length === 0 || activityId === "0" || explicitActivityIds.has(activityId)) {
11404
12107
  return REJECTION_REASON.INVALID;
11405
12108
  }
11406
12109
  explicitActivityIds.add(activityId);
@@ -11422,7 +12125,7 @@ function validateBatch(action, deps) {
11422
12125
  }
11423
12126
  if (link.linkId !== void 0) {
11424
12127
  const linkId = String(link.linkId);
11425
- if (linkId.length === 0 || explicitLinkIds.has(linkId) || action.mode === "append" && deps.adapter.getLink(linkId) !== null) {
12128
+ if (linkId.length === 0 || explicitLinkIds.has(linkId)) {
11426
12129
  return REJECTION_REASON.INVALID;
11427
12130
  }
11428
12131
  explicitLinkIds.add(linkId);
@@ -11458,24 +12161,38 @@ function hasParentCycle(items) {
11458
12161
  return false;
11459
12162
  }
11460
12163
  function buildBatchPlan(action, deps) {
12164
+ const normalizedOverrides = /* @__PURE__ */ new Map();
11461
12165
  for (const item of action.items) {
11462
- if (item.activityId !== void 0) {
11463
- deps.activityIdGen.bump(item.activityId);
11464
- }
12166
+ const normalized2 = normalizeActivityCreationOverrides(
12167
+ item.overrides,
12168
+ deps.sector.hoursPerDay
12169
+ );
12170
+ if (!normalized2.ok) return normalized2;
12171
+ normalizedOverrides.set(item.batchId, normalized2.value);
11465
12172
  }
11466
12173
  const activityIdsByBatchId = /* @__PURE__ */ new Map();
12174
+ const plannedActivityIds = /* @__PURE__ */ new Set();
11467
12175
  for (const item of action.items) {
11468
- activityIdsByBatchId.set(
11469
- item.batchId,
11470
- item.activityId ?? deps.activityIdGen.next()
12176
+ const activityId = resolvePlannedActivityId(
12177
+ item.activityId,
12178
+ action.mode,
12179
+ plannedActivityIds,
12180
+ deps
11471
12181
  );
11472
- }
11473
- for (const link of action.links) {
11474
- if (link.linkId !== void 0) deps.linkIdGen.bump(link.linkId);
11475
- }
11476
- const linkIds = action.links.map(
11477
- (link) => link.linkId ?? deps.linkIdGen.next()
11478
- );
12182
+ plannedActivityIds.add(String(activityId));
12183
+ activityIdsByBatchId.set(item.batchId, activityId);
12184
+ }
12185
+ const plannedLinkIds = /* @__PURE__ */ new Set();
12186
+ const linkIds = action.links.map((link) => {
12187
+ const linkId = resolvePlannedLinkId(
12188
+ link.linkId,
12189
+ action.mode,
12190
+ plannedLinkIds,
12191
+ deps
12192
+ );
12193
+ plannedLinkIds.add(String(linkId));
12194
+ return linkId;
12195
+ });
11479
12196
  const insertionCorrelativeIds = planInsertionCorrelatives(
11480
12197
  action,
11481
12198
  activityIdsByBatchId,
@@ -11498,7 +12215,8 @@ function buildBatchPlan(action, deps) {
11498
12215
  activityId,
11499
12216
  parentId,
11500
12217
  parentBatchId,
11501
- insertionCorrelativeId
12218
+ insertionCorrelativeId,
12219
+ overrides: normalizedOverrides.get(item.batchId) ?? {}
11502
12220
  });
11503
12221
  }
11504
12222
  const links = [];
@@ -11519,6 +12237,30 @@ function buildBatchPlan(action, deps) {
11519
12237
  }
11520
12238
  return { ok: true, plan: { items, links } };
11521
12239
  }
12240
+ function resolvePlannedActivityId(explicitId, mode, plannedIds, deps) {
12241
+ const explicitIsAvailable = explicitId !== void 0 && (mode === "replace" || deps.adapter.getActivity(explicitId) === null && !deps.isActivityIdReserved(String(explicitId))) && !plannedIds.has(String(explicitId));
12242
+ if (explicitIsAvailable) {
12243
+ deps.activityIdGen.bump(explicitId);
12244
+ return explicitId;
12245
+ }
12246
+ let candidate = deps.activityIdGen.next();
12247
+ while (plannedIds.has(String(candidate)) || mode === "append" && (deps.adapter.getActivity(candidate) !== null || deps.isActivityIdReserved(String(candidate)))) {
12248
+ candidate = deps.activityIdGen.next();
12249
+ }
12250
+ return candidate;
12251
+ }
12252
+ function resolvePlannedLinkId(explicitId, mode, plannedIds, deps) {
12253
+ const explicitIsAvailable = explicitId !== void 0 && (mode === "replace" || deps.adapter.getLink(explicitId) === null && !deps.isLinkIdReserved(String(explicitId))) && !plannedIds.has(String(explicitId));
12254
+ if (explicitIsAvailable) {
12255
+ deps.linkIdGen.bump(explicitId);
12256
+ return explicitId;
12257
+ }
12258
+ let candidate = deps.linkIdGen.next();
12259
+ while (plannedIds.has(String(candidate)) || mode === "append" && (deps.adapter.getLink(candidate) !== null || deps.isLinkIdReserved(String(candidate)))) {
12260
+ candidate = deps.linkIdGen.next();
12261
+ }
12262
+ return candidate;
12263
+ }
11522
12264
  function planInsertionCorrelatives(action, activityIdsByBatchId, deps) {
11523
12265
  if (isSimpleRootAppend(action)) {
11524
12266
  const firstCorrelativeId = deps.adapter.activityCount();
@@ -11628,7 +12370,7 @@ async function createPlannedActivities(action, plannedItems, deps) {
11628
12370
  {
11629
12371
  parentId: item.parentId,
11630
12372
  activityId: item.activityId,
11631
- ...item.input.overrides === void 0 ? {} : { overrides: item.input.overrides },
12373
+ overrides: item.overrides,
11632
12374
  ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
11633
12375
  },
11634
12376
  deps,
@@ -11670,180 +12412,299 @@ function stringKeyedLinks(links) {
11670
12412
  return new Map(Array.from(links, ([linkId, link]) => [String(linkId), link]));
11671
12413
  }
11672
12414
 
11673
- // src/dispatch/handlers/activity-paste.ts
11674
- function resolvePasteRootDestination(destination, adapter) {
11675
- if ("afterSiblingId" in destination) {
11676
- const sibling = adapter.getActivity(destination.afterSiblingId);
11677
- if (!sibling) {
11678
- return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
11679
- }
11680
- const parentId2 = sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId);
11681
- return { ok: true, parentId: parentId2, firstAfter: destination.afterSiblingId };
12415
+ // src/dispatch/handlers/sir-sync.ts
12416
+ function dispatchSirSync(action, deps) {
12417
+ const { adapter } = deps;
12418
+ const id = action.activityId;
12419
+ const snapshot = adapter.getActivity(id);
12420
+ if (!snapshot) {
12421
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11682
12422
  }
11683
- const parentId = destination.parentId;
11684
- const siblings = collectSiblings(adapter, parentId).sort(
11685
- (a, b) => readCorrelativeId(a) - readCorrelativeId(b)
11686
- );
11687
- const before = siblings[destination.index]?.id;
11688
- return { ok: true, parentId, firstBefore: before };
11689
- }
11690
- function applyPasteDomainSemantics(overrides) {
11691
- const { customId: _customId, ...content } = overrides;
12423
+ const before = snapshot.pendingRequestIds ?? [];
12424
+ const after = action.pendingRequests.map((r) => r.id);
12425
+ adapter.setActivityField(action.activityId, "pendingRequestIds", after);
12426
+ const updated = adapter.getActivity(id);
12427
+ if (!updated) {
12428
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
12429
+ }
12430
+ const change = {
12431
+ id,
12432
+ kind: "updated",
12433
+ fields: { pendingRequestIds: { before, after } },
12434
+ // ISSUE-025: `after` must be the COMPLETE post-change entity; a partial
12435
+ // object crashes any consumer that projects the full snapshot.
12436
+ after: structuredCloneActivity(updated)
12437
+ };
11692
12438
  return {
11693
- ...content,
11694
- progress: 0,
11695
- ponderator: 0,
11696
- usedCost: 0,
11697
- workHours: 0,
11698
- isLookahead: false
12439
+ ok: true,
12440
+ changes: {
12441
+ source: action,
12442
+ activities: [change],
12443
+ links: [],
12444
+ calendars: [],
12445
+ trackingEvents: []
12446
+ }
11699
12447
  };
11700
12448
  }
11701
- async function dispatchActivityPaste(action, options, deps) {
11702
- const { adapter, scheduler, sector } = deps;
11703
- const rootDest = resolvePasteRootDestination(action.destination, adapter);
11704
- if (!rootDest.ok) return rootDest;
11705
- const beforeSnap = await snapshotActivities(adapter, /* @__PURE__ */ new Set());
11706
- const touched = /* @__PURE__ */ new Set();
11707
- const originalToNew = /* @__PURE__ */ new Map();
11708
- const createdIds = [];
11709
- let prevRootId;
11710
- for (const input of action.activities) {
11711
- const mappedParent = originalToNew.get(input.originalParentId);
11712
- let parentId;
11713
- let afterSiblingId;
11714
- let beforeSiblingId;
11715
- if (mappedParent !== void 0) {
11716
- parentId = mappedParent;
11717
- } else {
11718
- parentId = rootDest.parentId;
11719
- if (prevRootId === void 0) {
11720
- afterSiblingId = rootDest.firstAfter;
11721
- beforeSiblingId = rootDest.firstBefore;
11722
- } else {
11723
- afterSiblingId = prevRootId;
11724
- }
12449
+
12450
+ // src/propagations/upward/progress-rollup.ts
12451
+ function recomputeAllProgressRollup(adapter) {
12452
+ for (const parentId of parentsDeepestFirst(adapter)) {
12453
+ rollupParent(parentId, adapter);
12454
+ }
12455
+ }
12456
+ function recomputeProgressRollupForParents(parentIdsDeepestFirst, adapter) {
12457
+ for (const parentId of parentIdsDeepestFirst) {
12458
+ rollupParent(parentId, adapter);
12459
+ }
12460
+ }
12461
+ function parentsDeepestFirst(adapter) {
12462
+ const parents = /* @__PURE__ */ new Set();
12463
+ adapter.forEachActivity((_activity, id) => {
12464
+ if (adapter.getChildren(id).length > 0) parents.add(id);
12465
+ });
12466
+ const depthById = /* @__PURE__ */ new Map();
12467
+ for (const id of parents) depthById.set(id, depthOf2(id, adapter));
12468
+ return [...parents].sort(
12469
+ (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
12470
+ );
12471
+ }
12472
+ function depthOf2(id, adapter) {
12473
+ let depth = 0;
12474
+ let current = id;
12475
+ while (current && current !== "0") {
12476
+ if (!adapter.getActivity(current)) break;
12477
+ depth += 1;
12478
+ current = adapter.getParentId(current);
12479
+ }
12480
+ return depth;
12481
+ }
12482
+ function rollupParent(parentId, adapter) {
12483
+ const childIds = adapter.getChildren(parentId);
12484
+ if (childIds.length === 0) return;
12485
+ const children = [];
12486
+ for (const childId of childIds) {
12487
+ const child = adapter.getActivity(childId);
12488
+ if (!child) continue;
12489
+ children.push({
12490
+ progress: Number(child.progress ?? 0),
12491
+ ponderator: Number(child.ponderator ?? 0)
12492
+ });
12493
+ }
12494
+ const rollup = computeWeightedProgressRollup(children);
12495
+ if (rollup !== null) {
12496
+ adapter.setActivityField(
12497
+ parentId,
12498
+ COLUMN.PROGRESS,
12499
+ roundProgressPerLevel(rollup)
12500
+ );
12501
+ }
12502
+ }
12503
+
12504
+ // src/dispatch/handlers/persistence-acknowledge.ts
12505
+ function persistedOwnersOf(entities) {
12506
+ const owners = /* @__PURE__ */ new Map();
12507
+ for (const entity of entities) {
12508
+ const proplannerId = entity.proplannerId;
12509
+ if (typeof proplannerId === "number" && Number.isSafeInteger(proplannerId) && proplannerId > 0) {
12510
+ owners.set(proplannerId, String(entity.id));
11725
12511
  }
11726
- const core = await createActivityCore(
11727
- {
11728
- parentId,
11729
- afterSiblingId,
11730
- beforeSiblingId,
11731
- ...input.activityId ? { activityId: input.activityId } : {},
11732
- overrides: applyPasteDomainSemantics(input.overrides),
11733
- eventSource: action.eventSource
11734
- },
11735
- deps,
11736
- {
11737
- skipCorrelativeRecompute: true,
11738
- skipBeforeSnap: true,
11739
- customIdReferenceId: action.referenceActivityId
11740
- }
12512
+ }
12513
+ return owners;
12514
+ }
12515
+ function assertAllNewEntitiesAcknowledged(entities, assignments, entityName) {
12516
+ const acknowledgedIds = new Set(assignments.map(({ id }) => String(id)));
12517
+ for (const entity of entities) {
12518
+ if ((entity.proplannerId == null || entity.proplannerId === 0) && !acknowledgedIds.has(String(entity.id))) {
12519
+ throw new Error(
12520
+ `persistence-acknowledge: missing ${entityName} proplannerId for "${String(entity.id)}"`
12521
+ );
12522
+ }
12523
+ }
12524
+ }
12525
+ function assertValidAssignment(assignment, entityName, readCurrent, ownerByBackendId, seenIds, seenBackendIds) {
12526
+ const id = String(assignment.id);
12527
+ if (id.length === 0 || seenIds.has(id)) {
12528
+ throw new Error(
12529
+ `persistence-acknowledge: duplicate or empty ${entityName} id "${id}"`
11741
12530
  );
11742
- if (!core.ok) return core;
11743
- if (input.baselineSnapshot !== void 0) {
11744
- const snapshot = input.baselineSnapshot;
11745
- adapter.setActivityField(
11746
- core.newId,
11747
- "baselineSnapshot",
11748
- snapshot ? {
11749
- ...snapshot,
11750
- startDate: snapshot.startDate ? new Date(snapshot.startDate) : null,
11751
- endDate: snapshot.endDate ? new Date(snapshot.endDate) : null
11752
- } : null
12531
+ }
12532
+ if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0) {
12533
+ throw new Error(
12534
+ `persistence-acknowledge: invalid ${entityName} proplannerId ${String(assignment.proplannerId)}`
12535
+ );
12536
+ }
12537
+ if (seenBackendIds.has(assignment.proplannerId)) {
12538
+ throw new Error(
12539
+ `persistence-acknowledge: duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
12540
+ );
12541
+ }
12542
+ const state = readCurrent(id);
12543
+ if (!state.exists) {
12544
+ throw new Error(
12545
+ `persistence-acknowledge: unknown ${entityName} id "${id}"`
12546
+ );
12547
+ }
12548
+ if (state.current !== null && state.current !== 0 && state.current !== assignment.proplannerId) {
12549
+ throw new Error(
12550
+ `persistence-acknowledge: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
12551
+ );
12552
+ }
12553
+ const existingOwner = ownerByBackendId.get(assignment.proplannerId);
12554
+ if (existingOwner !== void 0 && existingOwner !== id) {
12555
+ throw new Error(
12556
+ `persistence-acknowledge: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
12557
+ );
12558
+ }
12559
+ seenIds.add(id);
12560
+ seenBackendIds.add(assignment.proplannerId);
12561
+ }
12562
+ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
12563
+ const seenIds = /* @__PURE__ */ new Set();
12564
+ const seenBackendIds = /* @__PURE__ */ new Set();
12565
+ for (const assignment of assignments) {
12566
+ assertValidAssignment(
12567
+ assignment,
12568
+ entityName,
12569
+ readCurrent,
12570
+ ownerByBackendId,
12571
+ seenIds,
12572
+ seenBackendIds
12573
+ );
12574
+ }
12575
+ }
12576
+ function assertValidReconciledProgress(entries, deps) {
12577
+ const seen = /* @__PURE__ */ new Set();
12578
+ for (const entry of entries) {
12579
+ const id = String(entry.activityId);
12580
+ if (id.length === 0 || seen.has(id)) {
12581
+ throw new Error(
12582
+ `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
12583
+ );
12584
+ }
12585
+ if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
12586
+ throw new Error(
12587
+ `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
11753
12588
  );
11754
12589
  }
11755
- originalToNew.set(input.originalId, core.newId);
11756
- createdIds.push(core.newId);
11757
- if (mappedParent === void 0) prevRootId = core.newId;
12590
+ if (!deps.adapter.getActivity(id)) {
12591
+ throw new Error(
12592
+ `persistence-acknowledge: unknown reconciled activity id "${id}"`
12593
+ );
12594
+ }
12595
+ seen.add(id);
11758
12596
  }
11759
- const correlativeShifts = recomputeCorrelativeIds(adapter);
11760
- const createdIdSet = new Set(createdIds.map(String));
11761
- foldCorrelativeShifts(
11762
- adapter,
11763
- correlativeShifts,
11764
- createdIdSet,
11765
- beforeSnap,
11766
- touched
12597
+ }
12598
+ async function applyReconciledProgress(entries, deps) {
12599
+ const progressToApply = entries.filter((entry) => {
12600
+ const current = deps.adapter.getActivity(String(entry.activityId));
12601
+ return current !== null && current.progress !== entry.progress;
12602
+ });
12603
+ const progressIds = new Set(
12604
+ progressToApply.map((entry) => String(entry.activityId))
11767
12605
  );
11768
- for (const createdId of createdIds) touched.add(String(createdId));
11769
- const appliedLinks = [];
11770
- for (const link of action.links) {
11771
- const newSource = originalToNew.get(link.source);
11772
- const newTarget = originalToNew.get(link.target);
11773
- if (newSource === void 0 || newTarget === void 0) continue;
11774
- const op = {
11775
- kind: "create",
11776
- source: newSource,
11777
- target: newTarget,
11778
- type: link.type,
11779
- lag: lagDaysToHours(link.lag, sector.hoursPerDay)
11780
- };
11781
- const res = applyLinkOperation(op, {
11782
- port: adapter,
11783
- newLinkId: link.linkId ? () => link.linkId : deps.linkIdGen.next
11784
- });
11785
- appliedLinks.push({
11786
- op,
11787
- finalLinkId: res.applied ? res.linkId : null,
11788
- rejected: res.applied ? null : res.rejected ?? "link_op_rejected"
11789
- });
12606
+ const ancestors = findAncestorsOfDirty(deps.adapter, progressIds);
12607
+ const touched = new Set(progressIds);
12608
+ for (const parentId of ancestors) {
12609
+ touched.add(String(parentId));
11790
12610
  }
11791
- const { scheduledIds } = await runPostMutation(
11792
- {
11793
- adapter,
11794
- scheduler,
11795
- sector: deps.sector,
11796
- defaultBaseCalendarId: deps.defaultBaseCalendarId
11797
- },
11798
- {
11799
- action,
11800
- autoscheduleFrom: createdIds[0] ?? null,
11801
- recomputeParentsFrom: createdIds,
11802
- now: deps.now,
11803
- options
12611
+ const before = await snapshotActivities(deps.adapter, touched);
12612
+ deps.adapter.batchUpdate(() => {
12613
+ for (const entry of progressToApply) {
12614
+ deps.adapter.setActivityField(
12615
+ String(entry.activityId),
12616
+ "progress",
12617
+ entry.progress
12618
+ );
11804
12619
  }
12620
+ recomputeProgressRollupForParents(ancestors, deps.adapter);
12621
+ recomputeRealWorkFromProgress(progressIds, ancestors, deps.adapter);
12622
+ });
12623
+ return [...await buildActivityChanges(deps.adapter, before, touched)];
12624
+ }
12625
+ async function dispatchPersistenceAcknowledge(action, deps) {
12626
+ const activities = action.activities ?? [];
12627
+ const links = action.links ?? [];
12628
+ const reconciledProgress = action.reconciledProgress ?? [];
12629
+ assertValidReconciledProgress(reconciledProgress, deps);
12630
+ const currentActivities = deps.adapter.getAllActivities();
12631
+ const currentLinks = deps.adapter.getAllLinks();
12632
+ const activityOwnerByBackendId = persistedOwnersOf(currentActivities);
12633
+ const linkOwnerByBackendId = persistedOwnersOf(currentLinks);
12634
+ assertValidAssignments(
12635
+ activities,
12636
+ "activity",
12637
+ (id) => {
12638
+ const activity = deps.adapter.getActivity(id);
12639
+ return {
12640
+ exists: activity !== null,
12641
+ current: activity?.proplannerId ?? null
12642
+ };
12643
+ },
12644
+ activityOwnerByBackendId
11805
12645
  );
11806
- const linkChanges = buildLinkChangesForBatch(
11807
- appliedLinks,
11808
- /* @__PURE__ */ new Map(),
11809
- adapter
12646
+ assertValidAssignments(
12647
+ links,
12648
+ "link",
12649
+ (id) => {
12650
+ const link = deps.adapter.getLink(id);
12651
+ return { exists: link !== null, current: link?.proplannerId ?? null };
12652
+ },
12653
+ linkOwnerByBackendId
11810
12654
  );
11811
- return {
11812
- ok: true,
11813
- changes: await assembleChangeSet(adapter, {
11814
- source: action,
11815
- beforeSnap,
11816
- touchedIds: touched,
11817
- scheduledIds,
11818
- links: linkChanges,
11819
- hoursPerDay: sector.hoursPerDay
11820
- })
11821
- };
11822
- }
11823
-
11824
- // src/dispatch/handlers/sir-sync.ts
11825
- function dispatchSirSync(action, deps) {
11826
- const { adapter } = deps;
11827
- const id = action.activityId;
11828
- const snapshot = adapter.getActivity(id);
11829
- if (!snapshot) {
11830
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11831
- }
11832
- const before = snapshot.pendingRequestIds ?? [];
11833
- const after = action.pendingRequests.map((r) => r.id);
11834
- adapter.setActivityField(action.activityId, "pendingRequestIds", after);
11835
- const change = {
11836
- id,
11837
- kind: "updated",
11838
- fields: { pendingRequestIds: { before, after } },
11839
- after: { id, pendingRequestIds: after }
11840
- };
12655
+ assertAllNewEntitiesAcknowledged(currentActivities, activities, "activity");
12656
+ assertAllNewEntitiesAcknowledged(currentLinks, links, "link");
12657
+ const activityChanges = await applyReconciledProgress(
12658
+ reconciledProgress,
12659
+ deps
12660
+ );
12661
+ const linkChanges = [];
12662
+ deps.adapter.batchUpdate(() => {
12663
+ for (const assignment of activities) {
12664
+ const before = deps.adapter.getActivity(assignment.id);
12665
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12666
+ const previousProplannerId = before.proplannerId;
12667
+ deps.adapter.setActivityField(
12668
+ assignment.id,
12669
+ "proplannerId",
12670
+ assignment.proplannerId
12671
+ );
12672
+ activityChanges.push({
12673
+ id: assignment.id,
12674
+ kind: "updated",
12675
+ fields: {
12676
+ proplannerId: {
12677
+ before: previousProplannerId,
12678
+ after: assignment.proplannerId
12679
+ }
12680
+ },
12681
+ after: { ...before, proplannerId: assignment.proplannerId }
12682
+ });
12683
+ }
12684
+ for (const assignment of links) {
12685
+ const before = deps.adapter.getLink(assignment.id);
12686
+ if (!before || before.proplannerId === assignment.proplannerId) continue;
12687
+ const previousProplannerId = before.proplannerId;
12688
+ deps.adapter.setLinkProplannerId(assignment.id, assignment.proplannerId);
12689
+ linkChanges.push({
12690
+ id: assignment.id,
12691
+ kind: "updated",
12692
+ fields: {
12693
+ proplannerId: {
12694
+ before: previousProplannerId,
12695
+ after: assignment.proplannerId
12696
+ }
12697
+ },
12698
+ after: { ...before, proplannerId: assignment.proplannerId }
12699
+ });
12700
+ }
12701
+ });
11841
12702
  return {
11842
12703
  ok: true,
11843
12704
  changes: {
11844
12705
  source: action,
11845
- activities: [change],
11846
- links: [],
12706
+ activities: activityChanges,
12707
+ links: linkChanges,
11847
12708
  calendars: [],
11848
12709
  trackingEvents: []
11849
12710
  }
@@ -11851,8 +12712,15 @@ function dispatchSirSync(action, deps) {
11851
12712
  }
11852
12713
 
11853
12714
  // src/dispatch/handlers/activity-lookahead-sync.ts
11854
- function dispatchActivityLookaheadSync(action, deps) {
12715
+ async function dispatchActivityLookaheadSync(action, options, deps) {
12716
+ if (hasInvalidPersistedProgress(action)) {
12717
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
12718
+ }
11855
12719
  const ids = [...new Set(action.activityIds.map(String))];
12720
+ const acknowledgesUnknownPersistedActivity = action.acknowledgePersistedLookahead === true && ids.some((activityId) => !deps.isActivityIdReserved(activityId));
12721
+ if (acknowledgesUnknownPersistedActivity) {
12722
+ return { ok: false, reason: REJECTION_REASON.INVALID };
12723
+ }
11856
12724
  const snapshots = ids.map((id) => deps.adapter.getActivity(id));
11857
12725
  if (snapshots.some((snapshot) => !snapshot)) {
11858
12726
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
@@ -11892,6 +12760,39 @@ function dispatchActivityLookaheadSync(action, deps) {
11892
12760
  }
11893
12761
  });
11894
12762
  }
12763
+ if (action.persistedProgress !== void 0) {
12764
+ const progressChanges = await applyReconciledProgress(
12765
+ [{ activityId: ids[0], progress: action.persistedProgress }],
12766
+ deps
12767
+ );
12768
+ if (progressChanges.length > 0) {
12769
+ await runPostMutation(
12770
+ {
12771
+ adapter: deps.adapter,
12772
+ scheduler: deps.scheduler,
12773
+ sector: deps.sector,
12774
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
12775
+ },
12776
+ {
12777
+ action,
12778
+ autoscheduleFrom: null,
12779
+ recomputeParentsFrom: [],
12780
+ now: deps.now,
12781
+ options
12782
+ }
12783
+ );
12784
+ return {
12785
+ ok: true,
12786
+ changes: await assembleChangeSet(deps.adapter, {
12787
+ source: action,
12788
+ beforeSnap: /* @__PURE__ */ new Map(),
12789
+ touchedIds: [],
12790
+ scheduledIds: [],
12791
+ hoursPerDay: deps.sector.hoursPerDay
12792
+ })
12793
+ };
12794
+ }
12795
+ }
11895
12796
  return {
11896
12797
  ok: true,
11897
12798
  changes: {
@@ -11903,6 +12804,10 @@ function dispatchActivityLookaheadSync(action, deps) {
11903
12804
  }
11904
12805
  };
11905
12806
  }
12807
+ function hasInvalidPersistedProgress(action) {
12808
+ const progress = action.persistedProgress;
12809
+ return progress !== void 0 && (action.activityIds.length !== 1 || !Number.isFinite(progress) || progress < 0 || progress > 100);
12810
+ }
11906
12811
 
11907
12812
  // src/internal/hierarchy/invalid-links.ts
11908
12813
  function collectInvalidLinkIdsForCycle(movedActivityIds, adapter) {
@@ -11936,6 +12841,37 @@ function isAncestor(descendantId, candidateAncestorId, adapter) {
11936
12841
  return false;
11937
12842
  }
11938
12843
 
12844
+ // src/dispatch/shared/hierarchy-cycle-guard.ts
12845
+ function reparentClosesEffectiveCycle(adapter, reparented) {
12846
+ const links = adapter.getAllLinks();
12847
+ const verdict = validateTopologyCommit({
12848
+ port: adapter,
12849
+ currentLinks: links,
12850
+ proposedLinks: links,
12851
+ proposal: { reparented }
12852
+ });
12853
+ return !verdict.ok;
12854
+ }
12855
+ function collectCyclicReparents(planned, adapter) {
12856
+ const cyclic = /* @__PURE__ */ new Set();
12857
+ if (planned.length === 0) return cyclic;
12858
+ const combined = new Map(
12859
+ planned.map((reparent) => [reparent.activityId, reparent.newParentId])
12860
+ );
12861
+ if (!reparentClosesEffectiveCycle(adapter, combined)) return cyclic;
12862
+ const accepted = /* @__PURE__ */ new Map();
12863
+ for (const reparent of planned) {
12864
+ const trial = new Map(accepted);
12865
+ trial.set(reparent.activityId, reparent.newParentId);
12866
+ if (reparentClosesEffectiveCycle(adapter, trial)) {
12867
+ cyclic.add(reparent.activityId);
12868
+ continue;
12869
+ }
12870
+ accepted.set(reparent.activityId, reparent.newParentId);
12871
+ }
12872
+ return cyclic;
12873
+ }
12874
+
11939
12875
  // src/dispatch/handlers/activity-move.ts
11940
12876
  async function dispatchActivityMove(action, options, deps) {
11941
12877
  const { adapter, scheduler, sector } = deps;
@@ -11977,6 +12913,17 @@ async function dispatchActivityMove(action, options, deps) {
11977
12913
  const oldParentKey = normalizeParentKey(oldParent);
11978
12914
  const newParentKey = String(action.parentId);
11979
12915
  const parentChanged = oldParentKey !== newParentKey;
12916
+ if (parentChanged) {
12917
+ const reparented = /* @__PURE__ */ new Map([
12918
+ [
12919
+ action.activityId,
12920
+ newParentIsRoot ? null : action.parentId
12921
+ ]
12922
+ ]);
12923
+ if (reparentClosesEffectiveCycle(adapter, reparented)) {
12924
+ return { ok: false, reason: REJECTION_REASON.HIERARCHY_CYCLE };
12925
+ }
12926
+ }
11980
12927
  const touched = /* @__PURE__ */ new Set([action.activityId]);
11981
12928
  if (parentChanged) {
11982
12929
  if (oldParentKey !== ROOT_PARENT_ID) touched.add(oldParentKey);
@@ -12134,21 +13081,14 @@ async function dispatchActivityMove(action, options, deps) {
12134
13081
  };
12135
13082
  }
12136
13083
 
12137
- // src/dispatch/handlers/activity-indent.ts
12138
- async function dispatchActivityIndent(action, options, deps) {
12139
- const { adapter, scheduler, sector } = deps;
12140
- scheduler.invalidateAllCaches();
12141
- if (action.activityIds.length === 0) {
12142
- return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
12143
- }
12144
- const selectedSet = new Set(action.activityIds);
12145
- const touched = /* @__PURE__ */ new Set();
13084
+ // src/dispatch/shared/indent-planning.ts
13085
+ function planIndentMoves(activityIds, adapter) {
13086
+ const selectedSet = new Set(activityIds);
12146
13087
  const failed = [];
12147
- const succeededIds = [];
12148
13088
  const moves = [];
12149
13089
  const plannedIds = /* @__PURE__ */ new Set();
12150
13090
  const blockingSet = new Set(selectedSet);
12151
- const sortedIds = [...action.activityIds].sort((a, b) => {
13091
+ const sortedIds = [...activityIds].sort((a, b) => {
12152
13092
  const aSnap = adapter.getActivity(a);
12153
13093
  const bSnap = adapter.getActivity(b);
12154
13094
  const ac = aSnap ? readCorrelativeId(aSnap) : NOT_SET_SORT_VALUE2;
@@ -12183,12 +13123,43 @@ async function dispatchActivityIndent(action, options, deps) {
12183
13123
  const oldParentId = isRootParent(oldParentRaw) ? ROOT_PARENT_ID : String(oldParentRaw);
12184
13124
  moves.push({ activityId: id, newParentId: targetParentId, oldParentId });
12185
13125
  plannedIds.add(id);
12186
- touched.add(id);
12187
- touched.add(targetParentId);
12188
- if (oldParentId !== ROOT_PARENT_ID) touched.add(oldParentId);
12189
13126
  }
13127
+ const cyclicMoveIds = collectCyclicReparents(moves, adapter);
13128
+ for (const cyclicId of cyclicMoveIds) {
13129
+ failed.push({
13130
+ activityId: cyclicId,
13131
+ reason: REJECTION_REASON.HIERARCHY_CYCLE
13132
+ });
13133
+ }
13134
+ const allowedMoves = moves.filter(
13135
+ (move) => !cyclicMoveIds.has(move.activityId)
13136
+ );
13137
+ return { moves, cyclicMoveIds, allowedMoves, failed };
13138
+ }
13139
+
13140
+ // src/dispatch/handlers/activity-indent.ts
13141
+ async function dispatchActivityIndent(action, options, deps) {
13142
+ const { adapter, scheduler, sector } = deps;
13143
+ scheduler.invalidateAllCaches();
13144
+ if (action.activityIds.length === 0) {
13145
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
13146
+ }
13147
+ const touched = /* @__PURE__ */ new Set();
13148
+ const succeededIds = [];
13149
+ const plan = planIndentMoves(action.activityIds, adapter);
13150
+ const failed = [
13151
+ ...plan.failed
13152
+ ];
13153
+ for (const move of plan.moves) {
13154
+ touched.add(String(move.activityId));
13155
+ touched.add(String(move.newParentId));
13156
+ if (move.oldParentId !== ROOT_PARENT_ID) {
13157
+ touched.add(String(move.oldParentId));
13158
+ }
13159
+ }
13160
+ const allowedMoves = plan.allowedMoves;
12190
13161
  const beforeSnap = await snapshotActivities(adapter, touched);
12191
- for (const { activityId, newParentId } of moves) {
13162
+ for (const { activityId, newParentId } of allowedMoves) {
12192
13163
  adapter.setActivityField(activityId, ACTIVITY_PROPERTY.PARENT, newParentId);
12193
13164
  adapter.setActivityField(
12194
13165
  activityId,
@@ -12198,7 +13169,7 @@ async function dispatchActivityIndent(action, options, deps) {
12198
13169
  succeededIds.push(activityId);
12199
13170
  }
12200
13171
  const promotedParents = /* @__PURE__ */ new Set();
12201
- for (const { activityId, newParentId } of moves) {
13172
+ for (const { activityId, newParentId } of allowedMoves) {
12202
13173
  if (promotedParents.has(newParentId)) continue;
12203
13174
  promotedParents.add(newParentId);
12204
13175
  const newParent = adapter.getActivity(newParentId);
@@ -12250,7 +13221,7 @@ async function dispatchActivityIndent(action, options, deps) {
12250
13221
  releaseClearedCustomId(oldNewParentCustomId, deps.customIdTracker);
12251
13222
  }
12252
13223
  const indentAffectedParents = /* @__PURE__ */ new Set();
12253
- for (const m of moves) {
13224
+ for (const m of allowedMoves) {
12254
13225
  if (m.oldParentId !== ROOT_PARENT_ID)
12255
13226
  indentAffectedParents.add(m.oldParentId);
12256
13227
  indentAffectedParents.add(m.newParentId);
@@ -12411,8 +13382,24 @@ async function dispatchActivityOutdent(action, options, deps) {
12411
13382
  if (grandparentKey !== ROOT_PARENT_ID) touched.add(String(grandparentKey));
12412
13383
  oldParents.add(oldParentKey);
12413
13384
  }
13385
+ const cyclicPlanIds = collectCyclicReparents(
13386
+ planned.map((plan) => ({
13387
+ activityId: plan.activityId,
13388
+ newParentId: plan.grandparentKey
13389
+ })),
13390
+ adapter
13391
+ );
13392
+ for (const cyclicId of cyclicPlanIds) {
13393
+ failed.push({
13394
+ activityId: cyclicId,
13395
+ reason: REJECTION_REASON.HIERARCHY_CYCLE
13396
+ });
13397
+ }
13398
+ const allowedPlans = planned.filter(
13399
+ (plan) => !cyclicPlanIds.has(plan.activityId)
13400
+ );
12414
13401
  const beforeSnap = await snapshotActivities(adapter, touched);
12415
- for (const plan of planned) {
13402
+ for (const plan of allowedPlans) {
12416
13403
  adapter.setActivityField(
12417
13404
  plan.activityId,
12418
13405
  ACTIVITY_PROPERTY.PARENT,
@@ -12446,7 +13433,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12446
13433
  task: parentActivity
12447
13434
  }),
12448
13435
  idsRemoved: new Set(
12449
- planned.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
13436
+ allowedPlans.filter((plan) => plan.oldParentKey === oldParentKey).map((plan) => plan.activityId)
12450
13437
  )
12451
13438
  });
12452
13439
  if (!demotion) {
@@ -12472,7 +13459,7 @@ async function dispatchActivityOutdent(action, options, deps) {
12472
13459
  for (const oldParentKey of oldParents) {
12473
13460
  outdentAffectedParents.add(String(oldParentKey));
12474
13461
  }
12475
- for (const plan of planned) {
13462
+ for (const plan of allowedPlans) {
12476
13463
  if (plan.grandparentKey !== ROOT_PARENT_ID) {
12477
13464
  outdentAffectedParents.add(String(plan.grandparentKey));
12478
13465
  }
@@ -12560,6 +13547,17 @@ async function dispatchActivitySetProgress(action, options, deps) {
12560
13547
  "[ScheduleCore] progressPipeline not registered \u2014 cannot dispatch activity-set-progress"
12561
13548
  );
12562
13549
  }
13550
+ const editGate = pipeline.canEdit(activitySnap, ctx.hierarchy);
13551
+ if (!editGate.allowed) {
13552
+ const declaredButtonOverride = action.origin === "action-button" && (editGate.reason === "has_lookahead_tasks" || editGate.reason === "summary");
13553
+ if (!declaredButtonOverride) {
13554
+ return {
13555
+ ok: false,
13556
+ reason: editGate.reason,
13557
+ ...editGate.alertKey ? { alertKey: editGate.alertKey } : {}
13558
+ };
13559
+ }
13560
+ }
12563
13561
  const changes = pipeline.transform(
12564
13562
  activitySnap,
12565
13563
  action.newValue,
@@ -12976,171 +13974,29 @@ function syncCustomIdTracker(edit, oldCustomId, deps) {
12976
13974
  const rawValue = liveActivity ? liveActivity.customId : null;
12977
13975
  const newCustomId = typeof rawValue === "string" ? rawValue : null;
12978
13976
  if (oldCustomId !== newCustomId) {
12979
- deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
12980
- }
12981
- }
12982
- function recordConstraintPrior(priors, edit, beforeSnap) {
12983
- if (!isConstraintEditColumn(edit.column)) return;
12984
- const activityKey = String(edit.activityId);
12985
- if (priors.has(activityKey)) return;
12986
- const preBatchSnapshot = beforeSnap.get(activityKey);
12987
- if (!preBatchSnapshot) return;
12988
- priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
12989
- }
12990
- function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
12991
- const missing = /* @__PURE__ */ new Set();
12992
- for (const candidateId of ids) {
12993
- if (!beforeSnap.has(candidateId)) missing.add(candidateId);
12994
- }
12995
- if (missing.size === 0) return;
12996
- for (const missingId of missing) {
12997
- const liveActivity = deps.adapter.getActivity(missingId);
12998
- if (liveActivity) {
12999
- beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
13000
- }
13001
- }
13002
- }
13003
-
13004
- // src/dispatch/handlers/persistence-acknowledge.ts
13005
- function assertValidAssignment(assignment, entityName, readCurrent, ownerByBackendId, seenIds, seenBackendIds) {
13006
- const id = String(assignment.id);
13007
- if (id.length === 0 || seenIds.has(id)) {
13008
- throw new Error(
13009
- `persistence-acknowledge: duplicate or empty ${entityName} id "${id}"`
13010
- );
13011
- }
13012
- if (!Number.isSafeInteger(assignment.proplannerId) || assignment.proplannerId <= 0) {
13013
- throw new Error(
13014
- `persistence-acknowledge: invalid ${entityName} proplannerId ${String(assignment.proplannerId)}`
13015
- );
13016
- }
13017
- if (seenBackendIds.has(assignment.proplannerId)) {
13018
- throw new Error(
13019
- `persistence-acknowledge: duplicate ${entityName} proplannerId ${String(assignment.proplannerId)}`
13020
- );
13021
- }
13022
- const state = readCurrent(id);
13023
- if (!state.exists) {
13024
- throw new Error(
13025
- `persistence-acknowledge: unknown ${entityName} id "${id}"`
13026
- );
13027
- }
13028
- if (state.current !== null && state.current !== assignment.proplannerId) {
13029
- throw new Error(
13030
- `persistence-acknowledge: ${entityName} "${id}" already has proplannerId ${String(state.current)}`
13031
- );
13032
- }
13033
- const existingOwner = ownerByBackendId.get(assignment.proplannerId);
13034
- if (existingOwner !== void 0 && existingOwner !== id) {
13035
- throw new Error(
13036
- `persistence-acknowledge: ${entityName} proplannerId ${String(assignment.proplannerId)} already belongs to "${existingOwner}"`
13037
- );
13977
+ deps.customIdTracker.trackCustomIdChange(oldCustomId, newCustomId);
13038
13978
  }
13039
- seenIds.add(id);
13040
- seenBackendIds.add(assignment.proplannerId);
13041
13979
  }
13042
- function assertValidAssignments(assignments, entityName, readCurrent, ownerByBackendId) {
13043
- const seenIds = /* @__PURE__ */ new Set();
13044
- const seenBackendIds = /* @__PURE__ */ new Set();
13045
- for (const assignment of assignments) {
13046
- assertValidAssignment(
13047
- assignment,
13048
- entityName,
13049
- readCurrent,
13050
- ownerByBackendId,
13051
- seenIds,
13052
- seenBackendIds
13053
- );
13054
- }
13980
+ function recordConstraintPrior(priors, edit, beforeSnap) {
13981
+ if (!isConstraintEditColumn(edit.column)) return;
13982
+ const activityKey = String(edit.activityId);
13983
+ if (priors.has(activityKey)) return;
13984
+ const preBatchSnapshot = beforeSnap.get(activityKey);
13985
+ if (!preBatchSnapshot) return;
13986
+ priors.set(activityKey, preBatchSnapshot.constraintDate ?? null);
13055
13987
  }
13056
- function dispatchPersistenceAcknowledge(action, deps) {
13057
- const activities = action.activities ?? [];
13058
- const links = action.links ?? [];
13059
- const activityOwnerByBackendId = /* @__PURE__ */ new Map();
13060
- for (const activity of deps.adapter.getAllActivities()) {
13061
- if (activity.proplannerId != null) {
13062
- activityOwnerByBackendId.set(activity.proplannerId, String(activity.id));
13063
- }
13988
+ function mergeBeforeSnapshots2(beforeSnap, ids, deps) {
13989
+ const missing = /* @__PURE__ */ new Set();
13990
+ for (const candidateId of ids) {
13991
+ if (!beforeSnap.has(candidateId)) missing.add(candidateId);
13064
13992
  }
13065
- const linkOwnerByBackendId = /* @__PURE__ */ new Map();
13066
- for (const link of deps.adapter.getAllLinks()) {
13067
- if (link.proplannerId != null) {
13068
- linkOwnerByBackendId.set(link.proplannerId, String(link.id));
13993
+ if (missing.size === 0) return;
13994
+ for (const missingId of missing) {
13995
+ const liveActivity = deps.adapter.getActivity(missingId);
13996
+ if (liveActivity) {
13997
+ beforeSnap.set(missingId, structuredCloneActivity(liveActivity));
13069
13998
  }
13070
13999
  }
13071
- assertValidAssignments(
13072
- activities,
13073
- "activity",
13074
- (id) => {
13075
- const activity = deps.adapter.getActivity(id);
13076
- return {
13077
- exists: activity !== null,
13078
- current: activity?.proplannerId ?? null
13079
- };
13080
- },
13081
- activityOwnerByBackendId
13082
- );
13083
- assertValidAssignments(
13084
- links,
13085
- "link",
13086
- (id) => {
13087
- const link = deps.adapter.getLink(id);
13088
- return { exists: link !== null, current: link?.proplannerId ?? null };
13089
- },
13090
- linkOwnerByBackendId
13091
- );
13092
- const activityChanges = [];
13093
- const linkChanges = [];
13094
- deps.adapter.batchUpdate(() => {
13095
- for (const assignment of activities) {
13096
- const before = deps.adapter.getActivity(assignment.id);
13097
- if (!before || before.proplannerId === assignment.proplannerId) continue;
13098
- const previousProplannerId = before.proplannerId;
13099
- deps.adapter.setActivityField(
13100
- assignment.id,
13101
- "proplannerId",
13102
- assignment.proplannerId
13103
- );
13104
- activityChanges.push({
13105
- id: assignment.id,
13106
- kind: "updated",
13107
- fields: {
13108
- proplannerId: {
13109
- before: previousProplannerId,
13110
- after: assignment.proplannerId
13111
- }
13112
- },
13113
- after: { ...before, proplannerId: assignment.proplannerId }
13114
- });
13115
- }
13116
- for (const assignment of links) {
13117
- const before = deps.adapter.getLink(assignment.id);
13118
- if (!before || before.proplannerId === assignment.proplannerId) continue;
13119
- const previousProplannerId = before.proplannerId;
13120
- deps.adapter.setLinkProplannerId(assignment.id, assignment.proplannerId);
13121
- linkChanges.push({
13122
- id: assignment.id,
13123
- kind: "updated",
13124
- fields: {
13125
- proplannerId: {
13126
- before: previousProplannerId,
13127
- after: assignment.proplannerId
13128
- }
13129
- },
13130
- after: { ...before, proplannerId: assignment.proplannerId }
13131
- });
13132
- }
13133
- });
13134
- return {
13135
- ok: true,
13136
- changes: {
13137
- source: action,
13138
- activities: activityChanges,
13139
- links: linkChanges,
13140
- calendars: [],
13141
- trackingEvents: []
13142
- }
13143
- };
13144
14000
  }
13145
14001
 
13146
14002
  // src/baselines/normalize-baseline-point.ts
@@ -13338,55 +14194,6 @@ function recomputeAllPonderators(criterion, adapter) {
13338
14194
  });
13339
14195
  }
13340
14196
 
13341
- // src/propagations/upward/progress-rollup.ts
13342
- function recomputeAllProgressRollup(adapter) {
13343
- for (const parentId of parentsDeepestFirst(adapter)) {
13344
- rollupParent(parentId, adapter);
13345
- }
13346
- }
13347
- function parentsDeepestFirst(adapter) {
13348
- const parents = /* @__PURE__ */ new Set();
13349
- adapter.forEachActivity((_activity, id) => {
13350
- if (adapter.getChildren(id).length > 0) parents.add(id);
13351
- });
13352
- const depthById = /* @__PURE__ */ new Map();
13353
- for (const id of parents) depthById.set(id, depthOf2(id, adapter));
13354
- return [...parents].sort(
13355
- (a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)
13356
- );
13357
- }
13358
- function depthOf2(id, adapter) {
13359
- let depth = 0;
13360
- let current = id;
13361
- while (current && current !== "0") {
13362
- if (!adapter.getActivity(current)) break;
13363
- depth += 1;
13364
- current = adapter.getParentId(current);
13365
- }
13366
- return depth;
13367
- }
13368
- function rollupParent(parentId, adapter) {
13369
- const childIds = adapter.getChildren(parentId);
13370
- if (childIds.length === 0) return;
13371
- const children = [];
13372
- for (const childId of childIds) {
13373
- const child = adapter.getActivity(childId);
13374
- if (!child) continue;
13375
- children.push({
13376
- progress: Number(child.progress ?? 0),
13377
- ponderator: Number(child.ponderator ?? 0)
13378
- });
13379
- }
13380
- const rollup = computeWeightedProgressRollup(children);
13381
- if (rollup !== null) {
13382
- adapter.setActivityField(
13383
- parentId,
13384
- COLUMN.PROGRESS,
13385
- roundProgressPerLevel(rollup)
13386
- );
13387
- }
13388
- }
13389
-
13390
14197
  // src/dispatch/shared/baseline-weighted-fields.ts
13391
14198
  function recomputeBaselineWeightedFields(deps, criterion) {
13392
14199
  recomputeAllPonderators(criterion, deps.adapter);
@@ -13490,9 +14297,207 @@ async function dispatchStatusCriteriaSet(action, deps) {
13490
14297
  ok: true,
13491
14298
  changes: await assembleChangeSet(deps.adapter, {
13492
14299
  source: action,
13493
- beforeSnap,
13494
- touchedIds: changedIds,
13495
- scheduledIds: [],
14300
+ beforeSnap,
14301
+ touchedIds: changedIds,
14302
+ scheduledIds: [],
14303
+ hoursPerDay: deps.sector.hoursPerDay
14304
+ })
14305
+ };
14306
+ }
14307
+
14308
+ // src/activity-copy-paste/policy.ts
14309
+ var ACTIVITY_COPY_FIELDS = [
14310
+ "name",
14311
+ "description",
14312
+ "type",
14313
+ "startDate",
14314
+ "durationHours",
14315
+ "constraintType",
14316
+ "constraintDate",
14317
+ "calendarId",
14318
+ "cost",
14319
+ "hasCustomPonderator",
14320
+ "subcontractId",
14321
+ "responsableIds",
14322
+ "tagIds"
14323
+ ];
14324
+
14325
+ // src/activity-copy-paste/copy.ts
14326
+ function copyOverrides(activity) {
14327
+ const overrides = {};
14328
+ for (const field of ACTIVITY_COPY_FIELDS) {
14329
+ Object.assign(overrides, { [field]: cloneDomainValue(activity[field]) });
14330
+ }
14331
+ return overrides;
14332
+ }
14333
+ function selectedInTreeOrder(adapter, selected) {
14334
+ const ordered = [];
14335
+ const visit = (id) => {
14336
+ if (selected.has(String(id))) ordered.push(id);
14337
+ for (const childId of adapter.getChildren(id)) visit(childId);
14338
+ };
14339
+ for (const rootId of adapter.getRootIds()) visit(rootId);
14340
+ return ordered;
14341
+ }
14342
+ function dispatchActivityCopy(action, deps) {
14343
+ const selected = new Set(deps.adapter.checkedIds().map(String));
14344
+ if (selected.size === 0) {
14345
+ return { ok: false, reason: REJECTION_REASON.NO_ACTIVITY_SELECTION };
14346
+ }
14347
+ const orderedIds = selectedInTreeOrder(deps.adapter, selected);
14348
+ deps.clipboard.value = {
14349
+ activities: orderedIds.map((id) => {
14350
+ const activity = deps.adapter.getActivity(id);
14351
+ if (activity === null)
14352
+ throw new Error(`Selected activity ${id} is missing`);
14353
+ return {
14354
+ originalId: id,
14355
+ originalParentId: activity.parentId ?? "0",
14356
+ overrides: copyOverrides(activity)
14357
+ };
14358
+ }),
14359
+ links: deps.adapter.getAllLinks().filter(
14360
+ (link) => selected.has(String(link.source)) && selected.has(String(link.target))
14361
+ ).map((link) => cloneDomainValue(link))
14362
+ };
14363
+ const selection = dispatchSelectionReplace(
14364
+ { kind: "selection-replace", activityIds: [] },
14365
+ { adapter: deps.adapter }
14366
+ );
14367
+ if (!selection.ok) return selection;
14368
+ return { ...selection, changes: { ...selection.changes, source: action } };
14369
+ }
14370
+ function dispatchActivityClipboardClear(action, clipboard) {
14371
+ clipboard.value = null;
14372
+ return {
14373
+ ok: true,
14374
+ changes: {
14375
+ source: action,
14376
+ activities: [],
14377
+ links: [],
14378
+ calendars: [],
14379
+ trackingEvents: []
14380
+ }
14381
+ };
14382
+ }
14383
+
14384
+ // src/activity-copy-paste/paste.ts
14385
+ function resolveDestination(afterSiblingId, adapter) {
14386
+ const sibling = adapter.getActivity(afterSiblingId);
14387
+ if (!sibling) {
14388
+ return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
14389
+ }
14390
+ return {
14391
+ ok: true,
14392
+ parentId: sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId)
14393
+ };
14394
+ }
14395
+ var invariantFailure = () => ({
14396
+ ok: false,
14397
+ reason: REJECTION_REASON.ACTIVITY_PASTE_INVARIANT_FAILED
14398
+ });
14399
+ async function dispatchActivityPasteCopied(action, options, deps) {
14400
+ const clipboard = deps.activityClipboard.value;
14401
+ if (clipboard === null) {
14402
+ return { ok: false, reason: REJECTION_REASON.CLIPBOARD_EMPTY };
14403
+ }
14404
+ const destination = resolveDestination(
14405
+ action.destination.afterSiblingId,
14406
+ deps.adapter
14407
+ );
14408
+ if (!destination.ok) return destination;
14409
+ const beforeSnapshot = await snapshotActivities(
14410
+ deps.adapter,
14411
+ /* @__PURE__ */ new Set()
14412
+ );
14413
+ const touched = /* @__PURE__ */ new Set();
14414
+ const originalToNew = /* @__PURE__ */ new Map();
14415
+ const createdIds = [];
14416
+ let previousRootId;
14417
+ for (const copied of clipboard.activities) {
14418
+ const mappedParent = originalToNew.get(copied.originalParentId);
14419
+ const created = await createActivityCore(
14420
+ {
14421
+ parentId: mappedParent ?? destination.parentId,
14422
+ ...mappedParent === void 0 ? {
14423
+ afterSiblingId: previousRootId ?? action.destination.afterSiblingId
14424
+ } : {},
14425
+ overrides: cloneDomainValue(copied.overrides),
14426
+ eventSource: action.eventSource
14427
+ },
14428
+ deps,
14429
+ {
14430
+ skipCorrelativeRecompute: true,
14431
+ skipBeforeSnap: true,
14432
+ customIdReferenceId: action.destination.afterSiblingId
14433
+ }
14434
+ );
14435
+ if (!created.ok) return created;
14436
+ originalToNew.set(copied.originalId, created.newId);
14437
+ createdIds.push(created.newId);
14438
+ if (mappedParent === void 0) previousRootId = created.newId;
14439
+ }
14440
+ const correlativeShifts = recomputeCorrelativeIds(deps.adapter);
14441
+ const createdIdSet = new Set(createdIds.map(String));
14442
+ foldCorrelativeShifts(
14443
+ deps.adapter,
14444
+ correlativeShifts,
14445
+ createdIdSet,
14446
+ beforeSnapshot,
14447
+ touched
14448
+ );
14449
+ for (const createdId of createdIds) touched.add(String(createdId));
14450
+ const appliedLinks = [];
14451
+ for (const copiedLink of clipboard.links) {
14452
+ const source = originalToNew.get(copiedLink.source);
14453
+ const target = originalToNew.get(copiedLink.target);
14454
+ if (source === void 0 || target === void 0) return invariantFailure();
14455
+ const operation = {
14456
+ kind: "create",
14457
+ source,
14458
+ target,
14459
+ type: copiedLink.type,
14460
+ lag: copiedLink.lag
14461
+ };
14462
+ const result = applyLinkOperation(operation, {
14463
+ port: deps.adapter,
14464
+ newLinkId: deps.linkIdGen.next
14465
+ });
14466
+ if (!result.applied) return invariantFailure();
14467
+ appliedLinks.push({
14468
+ op: operation,
14469
+ finalLinkId: result.linkId,
14470
+ rejected: null
14471
+ });
14472
+ }
14473
+ const { scheduledIds } = await runPostMutation(
14474
+ {
14475
+ adapter: deps.adapter,
14476
+ scheduler: deps.scheduler,
14477
+ sector: deps.sector,
14478
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
14479
+ },
14480
+ {
14481
+ action,
14482
+ autoscheduleFrom: createdIds[0] ?? null,
14483
+ recomputeParentsFrom: createdIds,
14484
+ now: deps.now,
14485
+ options
14486
+ }
14487
+ );
14488
+ const linkChanges = buildLinkChangesForBatch(
14489
+ appliedLinks,
14490
+ /* @__PURE__ */ new Map(),
14491
+ deps.adapter
14492
+ );
14493
+ return {
14494
+ ok: true,
14495
+ changes: await assembleChangeSet(deps.adapter, {
14496
+ source: action,
14497
+ beforeSnap: beforeSnapshot,
14498
+ touchedIds: touched,
14499
+ scheduledIds,
14500
+ links: linkChanges,
13496
14501
  hoursPerDay: deps.sector.hoursPerDay
13497
14502
  })
13498
14503
  };
@@ -13500,6 +14505,18 @@ async function dispatchStatusCriteriaSet(action, deps) {
13500
14505
 
13501
14506
  // src/dispatch/dispatch.ts
13502
14507
  async function performDispatch(action, options, deps) {
14508
+ if (action.kind === "activity-copy") {
14509
+ return dispatchActivityCopy(action, {
14510
+ adapter: deps.adapter,
14511
+ clipboard: deps.activityClipboard
14512
+ });
14513
+ }
14514
+ if (action.kind === "activity-clipboard-clear") {
14515
+ return dispatchActivityClipboardClear(action, deps.activityClipboard);
14516
+ }
14517
+ if (action.kind === "activity-paste-copied") {
14518
+ return dispatchActivityPasteCopied(action, options, deps);
14519
+ }
13503
14520
  if (action.kind === "activity-batch") {
13504
14521
  return dispatchActivityBatch(action, options, deps);
13505
14522
  }
@@ -13524,9 +14541,6 @@ async function performDispatch(action, options, deps) {
13524
14541
  if (action.kind === "activity-create") {
13525
14542
  return dispatchActivityCreate(action, options, deps);
13526
14543
  }
13527
- if (action.kind === "activity-paste") {
13528
- return dispatchActivityPaste(action, options, deps);
13529
- }
13530
14544
  if (action.kind === "activity-delete") {
13531
14545
  return dispatchActivityDelete(action, options, deps);
13532
14546
  }
@@ -13570,7 +14584,7 @@ async function performDispatch(action, options, deps) {
13570
14584
  return dispatchSirSync(action, deps);
13571
14585
  }
13572
14586
  if (action.kind === "activity-lookahead-sync") {
13573
- return dispatchActivityLookaheadSync(action, deps);
14587
+ return dispatchActivityLookaheadSync(action, options, deps);
13574
14588
  }
13575
14589
  if (action.kind === "inline-edit" && (action.column === COLUMN.CUSTOM_PREDECESSORS || action.column === COLUMN.CUSTOM_SUCESSORS)) {
13576
14590
  return dispatchInlineEditLinks(action, options, deps);
@@ -16720,7 +17734,7 @@ var ViewStateStore = class {
16720
17734
  };
16721
17735
 
16722
17736
  // src/internal/state/schedule-state.ts
16723
- var ScheduleState = class {
17737
+ var ScheduleState = class _ScheduleState {
16724
17738
  _activities = /* @__PURE__ */ new Map();
16725
17739
  _lastStartDate = /* @__PURE__ */ new Map();
16726
17740
  _promotionSnapshot = /* @__PURE__ */ new Map();
@@ -16742,22 +17756,52 @@ var ScheduleState = class {
16742
17756
  _promotionSnapshotBefore = null;
16743
17757
  _calendar;
16744
17758
  _flags;
17759
+ _calendarSnapshot;
16745
17760
  constructor(snapshot, buildOrderComparator = () => null) {
16746
17761
  this._buildOrderComparator = buildOrderComparator;
17762
+ this._calendarSnapshot = cloneDomainValue(snapshot.calendars);
16747
17763
  for (const activity of snapshot.activities) {
16748
17764
  this._activities.set(activity.id, activity);
16749
17765
  }
16750
17766
  for (const link of snapshot.links) {
16751
- this._links.set(String(link.id), link);
16752
- this._indexLink(link);
17767
+ const normalized2 = {
17768
+ ...link,
17769
+ freeSlackHours: link.freeSlackHours ?? null,
17770
+ isCritical: link.isCritical === true
17771
+ };
17772
+ this._links.set(String(link.id), normalized2);
17773
+ this._indexLink(normalized2);
16753
17774
  }
16754
17775
  this._calendar = new CalendarCalculator(
16755
- createCalendarSetup(snapshot.calendars)
17776
+ createCalendarSetup(this._calendarSnapshot)
16756
17777
  );
16757
17778
  this._flags = buildFlags(snapshot);
16758
17779
  this._hierarchy = new HierarchyIndex(this._activities);
16759
17780
  this._hierarchy.rebuild();
16760
17781
  }
17782
+ fork() {
17783
+ const fork = new _ScheduleState(
17784
+ {
17785
+ activities: this.getAllActivities().map(cloneCoreActivity),
17786
+ links: this.getAllLinks().map((link) => ({ ...link })),
17787
+ calendars: cloneDomainValue(this._calendarSnapshot)
17788
+ },
17789
+ this._buildOrderComparator
17790
+ );
17791
+ fork._viewState.restore(cloneDomainValue(this._viewState.snapshot()));
17792
+ fork._lastStartDate = new Map(
17793
+ Array.from(this._lastStartDate, ([id, date2]) => [id, new Date(date2)])
17794
+ );
17795
+ fork._promotionSnapshot = new Map(
17796
+ Array.from(this._promotionSnapshot, ([id, snapshot]) => [
17797
+ id,
17798
+ cloneDomainValue(snapshot)
17799
+ ])
17800
+ );
17801
+ fork._flags = cloneDomainValue(this._flags);
17802
+ fork._filterProjection = cloneDomainValue(this._filterProjection);
17803
+ return fork;
17804
+ }
16761
17805
  getAllActivities() {
16762
17806
  return Array.from(this._activities.values());
16763
17807
  }
@@ -16981,6 +18025,7 @@ var ScheduleState = class {
16981
18025
  if (!activity) return;
16982
18026
  this._writeCapture.note(String(activityId), activity);
16983
18027
  activity[field] = value;
18028
+ refreshCalendarDurationIfDateBearing(activity, field);
16984
18029
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16985
18030
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16986
18031
  }
@@ -16994,6 +18039,7 @@ var ScheduleState = class {
16994
18039
  }
16995
18040
  this._writeCapture.note(String(activityId), activity);
16996
18041
  Reflect.set(activity, field, coerceActivityFieldValue(field, value));
18042
+ refreshCalendarDurationIfDateBearing(activity, field);
16997
18043
  if (field === ACTIVITY_PROPERTY.PARENT) this._hierarchy.markDirty();
16998
18044
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
16999
18045
  }
@@ -17004,6 +18050,7 @@ var ScheduleState = class {
17004
18050
  let reparented = false;
17005
18051
  for (const field of Object.keys(fields)) {
17006
18052
  Reflect.set(activity, field, Reflect.get(fields, field));
18053
+ refreshCalendarDurationIfDateBearing(activity, field);
17007
18054
  if (field === ACTIVITY_PROPERTY.PARENT) reparented = true;
17008
18055
  if (fieldAffectsVisualOrder(field)) this._cache.visualOrderIds = null;
17009
18056
  }
@@ -17013,8 +18060,13 @@ var ScheduleState = class {
17013
18060
  this._writeCapture.noteLinkAdd(String(link.id));
17014
18061
  const superseded = this._links.get(String(link.id));
17015
18062
  if (superseded) this._deindexLink(superseded);
17016
- this._links.set(String(link.id), { ...link });
17017
- this._indexLink(link);
18063
+ const normalized2 = {
18064
+ ...link,
18065
+ freeSlackHours: link.freeSlackHours ?? null,
18066
+ isCritical: link.isCritical === true
18067
+ };
18068
+ this._links.set(String(link.id), normalized2);
18069
+ this._indexLink(normalized2);
17018
18070
  }
17019
18071
  _indexLink(link) {
17020
18072
  pushLinkRef(this._outgoing, String(link.source), link.id);
@@ -17167,11 +18219,17 @@ var ScheduleState = class {
17167
18219
  };
17168
18220
  var EMPTY_LINK_IDS = [];
17169
18221
  function isMutableLinkField(field) {
17170
- return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE;
18222
+ return field === LINK_PROPERTY.LAG || field === LINK_PROPERTY.TYPE || field === LINK_PROPERTY.FREE_SLACK_HOURS || field === LINK_PROPERTY.IS_CRITICAL;
17171
18223
  }
17172
18224
  function fieldAffectsVisualOrder(field) {
17173
18225
  return field === ACTIVITY_PROPERTY.PARENT || field === ACTIVITY_PROPERTY.CORRELATIVE_ID;
17174
18226
  }
18227
+ function refreshCalendarDurationIfDateBearing(activity, field) {
18228
+ if (field !== ACTIVITY_PROPERTY.START_DATE && field !== ACTIVITY_PROPERTY.END_DATE && field !== ACTIVITY_PROPERTY.TYPE) {
18229
+ return;
18230
+ }
18231
+ activity.calendarDuration = computeCalendarDuration(activity);
18232
+ }
17175
18233
  function detachLinkRef(refs, linkId) {
17176
18234
  return refs.filter((ref) => String(ref) !== String(linkId));
17177
18235
  }
@@ -17219,15 +18277,7 @@ function readChildren(state, parentId) {
17219
18277
  return out;
17220
18278
  }
17221
18279
  function readChildrenIds(state, parentId) {
17222
- const key = parentId;
17223
- if (key === ROOT_PARENT_ID) {
17224
- const roots = [];
17225
- state.forEachActivity((activity, id) => {
17226
- if (isRootParent(activity.parentId)) roots.push(id);
17227
- });
17228
- return roots;
17229
- }
17230
- return getChildrenInVisualOrder(key, state);
18280
+ return getChildrenInVisualOrder(parentId, state);
17231
18281
  }
17232
18282
  function readSelectedActivityIds(state) {
17233
18283
  return state.checkedIds().map(String);
@@ -17290,8 +18340,7 @@ function buildPipelineContext(adapter, options) {
17290
18340
  activityReader: createStateBackedActivityReader(adapter),
17291
18341
  dateFormat: options.dateFormat ?? "YYYY-MM-DD",
17292
18342
  customHours: options.customHours ?? {},
17293
- hoursPerDay: options.hoursPerDay,
17294
- inputUnit: options.inputUnit ?? "days"
18343
+ hoursPerDay: options.hoursPerDay
17295
18344
  };
17296
18345
  }
17297
18346
 
@@ -17301,15 +18350,26 @@ function willRunCriticalPath(action) {
17301
18350
  }
17302
18351
 
17303
18352
  // src/dispatch/undo/undo-entry.ts
17304
- function buildHistoryChangeSet(changes) {
17305
- const activitiesWithoutAfter = changes.activities.map((entry) => ({
18353
+ var DERIVED_LINK_FIELDS = /* @__PURE__ */ new Set(["freeSlackHours", "isCritical"]);
18354
+ function withoutDerivedFields(entry, derivedFields) {
18355
+ const fields = Object.fromEntries(
18356
+ Object.entries(entry.fields ?? {}).filter(
18357
+ ([field]) => !derivedFields.has(field)
18358
+ )
18359
+ );
18360
+ if (entry.kind === "updated" && Object.keys(fields).length === 0) return null;
18361
+ return {
17306
18362
  ...entry,
18363
+ ...entry.fields === void 0 ? {} : { fields },
17307
18364
  after: null
17308
- }));
17309
- const linksWithoutAfter = changes.links.map((entry) => ({
18365
+ };
18366
+ }
18367
+ function buildHistoryChangeSet(changes) {
18368
+ const activitiesWithoutAfter = changes.activities.map((entry) => ({
17310
18369
  ...entry,
17311
18370
  after: null
17312
18371
  }));
18372
+ const linksWithoutAfter = changes.links.map((entry) => withoutDerivedFields(entry, DERIVED_LINK_FIELDS)).filter((entry) => entry !== null);
17313
18373
  const source = changes.source.kind === "activity-batch" ? { ...changes.source, items: [], links: [] } : changes.source;
17314
18374
  return cloneDomainValue({
17315
18375
  ...changes,
@@ -17319,7 +18379,22 @@ function buildHistoryChangeSet(changes) {
17319
18379
  });
17320
18380
  }
17321
18381
  function buildUndoEntry(changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks) {
17322
- return { changeSet, beforeSnap, beforeLinks, afterSnap, afterLinks };
18382
+ return {
18383
+ changeSet,
18384
+ beforeSnap,
18385
+ beforeLinks: resetDerivedLinkSnapshots(beforeLinks),
18386
+ afterSnap,
18387
+ afterLinks: resetDerivedLinkSnapshots(afterLinks)
18388
+ };
18389
+ }
18390
+ function resetDerivedLinkSnapshots(snapshots) {
18391
+ if (!snapshots) return void 0;
18392
+ return new Map(
18393
+ Array.from(snapshots, ([id, link]) => [
18394
+ id,
18395
+ { ...link, freeSlackHours: null, isCritical: false }
18396
+ ])
18397
+ );
17323
18398
  }
17324
18399
  function captureCreatedSnapshots(state, changeSet) {
17325
18400
  const afterSnap = /* @__PURE__ */ new Map();
@@ -17373,7 +18448,7 @@ function mergeCoalesced(top, next) {
17373
18448
  }
17374
18449
  function getDispatchHistoryPolicy(action) {
17375
18450
  if (action.kind === "persistence-acknowledge") return "clear-on-success";
17376
- 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 === "selection-toggle" || action.kind === "selection-replace" || action.kind === "visibility-set" || action.kind === "filter-set" || action.kind === "sort-set") {
18451
+ 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") {
17377
18452
  return "skip";
17378
18453
  }
17379
18454
  return "record";
@@ -17617,7 +18692,7 @@ function applyChangeSetSide(state, entry, side) {
17617
18692
  // src/dispatch/undo/undo-recorder.ts
17618
18693
  var DEFAULT_MAX_DEPTH = 100;
17619
18694
  var COALESCE_WINDOW_MS = 600;
17620
- var UndoRecorder = class {
18695
+ var UndoRecorder = class _UndoRecorder {
17621
18696
  constructor(maxDepth = DEFAULT_MAX_DEPTH) {
17622
18697
  this.maxDepth = maxDepth;
17623
18698
  }
@@ -17626,6 +18701,14 @@ var UndoRecorder = class {
17626
18701
  redoStack = [];
17627
18702
  _lastKey = null;
17628
18703
  _lastTime = 0;
18704
+ fork() {
18705
+ const fork = new _UndoRecorder(this.maxDepth);
18706
+ fork.undoStack.push(...cloneDomainValue(this.undoStack));
18707
+ fork.redoStack.push(...cloneDomainValue(this.redoStack));
18708
+ fork._lastKey = this._lastKey;
18709
+ fork._lastTime = this._lastTime;
18710
+ return fork;
18711
+ }
17629
18712
  record(entry, coalesceKey = null, now = 0) {
17630
18713
  const top = this.undoStack[this.undoStack.length - 1];
17631
18714
  if (coalesceKey != null && coalesceKey === this._lastKey && now - this._lastTime <= COALESCE_WINDOW_MS && top !== void 0 && canCoalesce(top, entry)) {
@@ -17749,40 +18832,56 @@ function buildStateSnapshot(activities, links, calendars, baseCalendars = []) {
17749
18832
  }
17750
18833
 
17751
18834
  // src/boundary/save/activity-changes.ts
17752
- var PERSISTABLE_ACTIVITY_FIELDS = [
17753
- "id",
17754
- "proplannerId",
17755
- "parentId",
17756
- "name",
17757
- "description",
17758
- "type",
17759
- "startDate",
17760
- "endDate",
17761
- "durationHours",
17762
- "constraintType",
17763
- "constraintDate",
17764
- "autoScheduling",
17765
- "calendarId",
17766
- "assignedDefaultCalendar",
17767
- "dateOrigin",
17768
- "progress",
17769
- "cost",
17770
- "usedCost",
17771
- "realCost",
17772
- "workHours",
17773
- "realWorkHours",
17774
- "ponderator",
17775
- "hasCustomPonderator",
17776
- "correlativeId",
17777
- "uniqueCorrelativeId",
17778
- "customId",
17779
- "subcontractId",
17780
- "isLookahead",
17781
- "newActivityIds",
17782
- "hasNewActivities",
17783
- "responsableIds",
17784
- "tagIds"
17785
- ];
18835
+ var ACTIVITY_SAVE_FIELD_POLICY = {
18836
+ id: "ignored",
18837
+ proplannerId: "ignored",
18838
+ parentId: "tracked",
18839
+ name: "tracked",
18840
+ description: "tracked",
18841
+ type: "tracked",
18842
+ startDate: "tracked",
18843
+ endDate: "tracked",
18844
+ durationHours: "tracked",
18845
+ constraintType: "tracked",
18846
+ constraintDate: "tracked",
18847
+ autoScheduling: "tracked",
18848
+ calendarId: "tracked",
18849
+ assignedDefaultCalendar: "ignored",
18850
+ dateOrigin: "tracked",
18851
+ progress: "tracked",
18852
+ cost: "tracked",
18853
+ usedCost: "tracked",
18854
+ realCost: "tracked",
18855
+ workHours: "tracked",
18856
+ realWorkHours: "tracked",
18857
+ ponderator: "tracked",
18858
+ hasCustomPonderator: "tracked",
18859
+ correlativeId: "tracked",
18860
+ uniqueCorrelativeId: "tracked",
18861
+ customId: "tracked",
18862
+ subcontractId: "tracked",
18863
+ isLookahead: "tracked",
18864
+ hasLookaheadTasks: "ignored",
18865
+ newActivityIds: "tracked",
18866
+ hasNewActivities: "tracked",
18867
+ isNewActivity: "ignored",
18868
+ pendingRequestIds: "ignored",
18869
+ responsableIds: "tracked",
18870
+ tagIds: "tracked",
18871
+ baselinePoints: "ignored",
18872
+ baselineSnapshot: "ignored",
18873
+ recursiveDurationHours: "ignored",
18874
+ isCritical: "ignored",
18875
+ freeSlackHours: "ignored",
18876
+ calendarDuration: "ignored",
18877
+ expectedProgress: "ignored",
18878
+ expectedProgressBaseline: "ignored",
18879
+ status: "ignored",
18880
+ criticalPath: "ignored"
18881
+ };
18882
+ var PERSISTABLE_ACTIVITY_FIELDS = Object.keys(ACTIVITY_SAVE_FIELD_POLICY).filter((field) => {
18883
+ return ACTIVITY_SAVE_FIELD_POLICY[field] === "tracked";
18884
+ });
17786
18885
  var DATE_FIELDS2 = /* @__PURE__ */ new Set([
17787
18886
  "startDate",
17788
18887
  "endDate",
@@ -17810,7 +18909,6 @@ function snapshotFieldValue(field, value) {
17810
18909
  return value;
17811
18910
  }
17812
18911
  function activityChangedSince(current, saved) {
17813
- if (current.assignedDefaultCalendar) return true;
17814
18912
  for (const field of PERSISTABLE_ACTIVITY_FIELDS) {
17815
18913
  if (!fieldEquals(field, current[field], saved[field])) return true;
17816
18914
  }
@@ -17828,15 +18926,35 @@ function dateEquals(current, saved) {
17828
18926
  return currentTime === savedTime;
17829
18927
  }
17830
18928
  function idArrayEquals(current, saved) {
17831
- const currentIds = Array.isArray(current) ? [...current].map(String).sort() : [];
17832
- const savedIds = Array.isArray(saved) ? [...saved].map(String).sort() : [];
17833
- if (currentIds.length !== savedIds.length) return false;
17834
- return currentIds.every((value, index) => value === savedIds[index]);
18929
+ const currentIds = new Set(Array.isArray(current) ? current.map(String) : []);
18930
+ const savedIds = new Set(Array.isArray(saved) ? saved.map(String) : []);
18931
+ if (currentIds.size !== savedIds.size) return false;
18932
+ for (const currentId of currentIds) {
18933
+ if (!savedIds.has(currentId)) return false;
18934
+ }
18935
+ return true;
17835
18936
  }
17836
18937
 
17837
18938
  // src/boundary/save/link-changes.ts
18939
+ var LINK_SAVE_FIELD_POLICY = {
18940
+ id: "ignored",
18941
+ source: "tracked",
18942
+ target: "tracked",
18943
+ type: "tracked",
18944
+ lag: "tracked",
18945
+ freeSlackHours: "ignored",
18946
+ isCritical: "ignored",
18947
+ ganttId: "ignored",
18948
+ sectorId: "ignored",
18949
+ proplannerId: "ignored"
18950
+ };
18951
+ var TRACKED_LINK_FIELDS = Object.keys(LINK_SAVE_FIELD_POLICY).filter((field) => {
18952
+ return LINK_SAVE_FIELD_POLICY[field] === "tracked";
18953
+ });
17838
18954
  function linkChangedSince(baseline, current) {
17839
- return baseline.source !== current.source || baseline.target !== current.target || baseline.lag !== current.lag || baseline.type !== current.type;
18955
+ return TRACKED_LINK_FIELDS.some(
18956
+ (field) => baseline[field] !== current[field]
18957
+ );
17840
18958
  }
17841
18959
  function checkNoUpdatedLinks(baseline, current) {
17842
18960
  const baselineById = new Map(
@@ -17850,23 +18968,73 @@ function checkNoUpdatedLinks(baseline, current) {
17850
18968
  }
17851
18969
 
17852
18970
  // src/boundary/save/save-tracker.ts
17853
- var SaveTracker = class {
18971
+ function hasPersistedIdentity(proplannerId) {
18972
+ const isNewIdentity = proplannerId == null || proplannerId === 0;
18973
+ if (isNewIdentity) return false;
18974
+ const isValidPersistedIdentity = typeof proplannerId === "number" && Number.isSafeInteger(proplannerId) && proplannerId > 0;
18975
+ if (!isValidPersistedIdentity) {
18976
+ throw new Error(`invalid proplannerId ${String(proplannerId)}`);
18977
+ }
18978
+ return true;
18979
+ }
18980
+ function assertValidPersistedIdentities(activities, links) {
18981
+ for (const activity of activities) {
18982
+ hasPersistedIdentity(activity.proplannerId);
18983
+ }
18984
+ for (const link of links) {
18985
+ hasPersistedIdentity(link.proplannerId);
18986
+ }
18987
+ }
18988
+ var SaveTracker = class _SaveTracker {
18989
+ trackingEnabled;
17854
18990
  initialized = false;
17855
18991
  dirtyActivityIds = /* @__PURE__ */ new Set();
17856
18992
  dirtyLinkIds = /* @__PURE__ */ new Set();
18993
+ initialCalendarRepairIds = /* @__PURE__ */ new Set();
17857
18994
  linksAtLastSave = /* @__PURE__ */ new Map();
17858
18995
  activitiesAtLastSave = /* @__PURE__ */ new Map();
18996
+ constructor(trackingEnabled = true) {
18997
+ this.trackingEnabled = trackingEnabled;
18998
+ }
18999
+ fork() {
19000
+ const fork = new _SaveTracker(false);
19001
+ fork.initialized = this.initialized;
19002
+ for (const [linkId, link] of this.linksAtLastSave) {
19003
+ fork.linksAtLastSave.set(linkId, cloneDomainValue(link));
19004
+ }
19005
+ for (const [activityId, activity] of this.activitiesAtLastSave) {
19006
+ fork.activitiesAtLastSave.set(activityId, cloneDomainValue(activity));
19007
+ }
19008
+ return fork;
19009
+ }
17859
19010
  init(activities, links) {
19011
+ if (!this.trackingEnabled) return;
19012
+ assertValidPersistedIdentities(activities, links);
17860
19013
  this.snapshotActivities(activities);
17861
19014
  this.snapshotLinks(links);
19015
+ for (const activity of activities) {
19016
+ if (activity.assignedDefaultCalendar) {
19017
+ const activityId = String(activity.id);
19018
+ this.initialCalendarRepairIds.add(activityId);
19019
+ this.dirtyActivityIds.add(activityId);
19020
+ }
19021
+ }
17862
19022
  this.initialized = true;
17863
19023
  }
19024
+ acknowledge(activities, links) {
19025
+ if (!this.trackingEnabled) return;
19026
+ assertValidPersistedIdentities(activities, links);
19027
+ this.snapshotActivities(activities);
19028
+ this.snapshotLinks(links);
19029
+ }
17864
19030
  snapshotActivities(activities) {
19031
+ if (!this.trackingEnabled) return;
17865
19032
  this.activitiesAtLastSave.clear();
17866
19033
  this.dirtyActivityIds.clear();
19034
+ this.initialCalendarRepairIds.clear();
17867
19035
  for (const activity of activities) {
17868
19036
  const activityId = String(activity.id);
17869
- if (activity.proplannerId == null) {
19037
+ if (!hasPersistedIdentity(activity.proplannerId)) {
17870
19038
  this.dirtyActivityIds.add(activityId);
17871
19039
  continue;
17872
19040
  }
@@ -17886,11 +19054,12 @@ var SaveTracker = class {
17886
19054
  }
17887
19055
  }
17888
19056
  snapshotLinks(links) {
19057
+ if (!this.trackingEnabled) return;
17889
19058
  this.linksAtLastSave.clear();
17890
19059
  this.dirtyLinkIds.clear();
17891
19060
  for (const link of links) {
17892
19061
  const linkId = String(link.id);
17893
- if (link.proplannerId == null) {
19062
+ if (!hasPersistedIdentity(link.proplannerId)) {
17894
19063
  this.dirtyLinkIds.add(linkId);
17895
19064
  continue;
17896
19065
  }
@@ -17905,12 +19074,11 @@ var SaveTracker = class {
17905
19074
  }
17906
19075
  }
17907
19076
  recordChanges(activities, links, getActivity, getLink) {
17908
- if (!this.initialized) return;
19077
+ if (!this.trackingEnabled || !this.initialized) return;
17909
19078
  for (const change of activities) {
17910
19079
  const activityId = String(change.id);
17911
19080
  const current = getActivity(activityId);
17912
- const saved = this.activitiesAtLastSave.get(activityId);
17913
- if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || activityChangedSince(current, saved.persistable)) {
19081
+ if (this.activityIsDirty(activityId, current)) {
17914
19082
  this.dirtyActivityIds.add(activityId);
17915
19083
  } else {
17916
19084
  this.dirtyActivityIds.delete(activityId);
@@ -17919,81 +19087,147 @@ var SaveTracker = class {
17919
19087
  for (const change of links) {
17920
19088
  const linkId = String(change.id);
17921
19089
  const current = getLink(linkId);
17922
- const saved = this.linksAtLastSave.get(linkId);
17923
- if (current === null ? saved !== void 0 : current.proplannerId == null || saved === void 0 || linkChangedSince(saved, current)) {
19090
+ if (this.linkIsDirty(linkId, current)) {
17924
19091
  this.dirtyLinkIds.add(linkId);
17925
19092
  } else {
17926
19093
  this.dirtyLinkIds.delete(linkId);
17927
19094
  }
17928
19095
  }
17929
19096
  }
17930
- acknowledgePersistedLookahead(activityIds, getActivity) {
17931
- if (!this.initialized) return;
17932
- for (const activityId of activityIds) {
19097
+ activityIsDirty(activityId, current) {
19098
+ const saved = this.activitiesAtLastSave.get(activityId);
19099
+ if (current === null) return saved !== void 0;
19100
+ return this.initialCalendarRepairIds.has(activityId) || !hasPersistedIdentity(current.proplannerId) || saved === void 0 || activityChangedSince(current, saved.persistable);
19101
+ }
19102
+ linkIsDirty(linkId, current) {
19103
+ const saved = this.linksAtLastSave.get(linkId);
19104
+ if (current === null) return saved !== void 0;
19105
+ return !hasPersistedIdentity(current.proplannerId) || saved === void 0 || linkChangedSince(saved, current);
19106
+ }
19107
+ acknowledgePersistedLookahead(changes, getActivity) {
19108
+ if (!this.trackingEnabled || !this.initialized) return;
19109
+ const acknowledgements = changes.map((change) => {
19110
+ const activityId = typeof change === "string" ? change : String(change.id);
17933
19111
  const current = getActivity(activityId);
17934
19112
  const saved = this.activitiesAtLastSave.get(activityId);
17935
- if (current === null || saved === void 0) continue;
19113
+ const isUnknownPersistedActivity = current === null || saved === void 0;
19114
+ if (isUnknownPersistedActivity) {
19115
+ throw new Error(`unknown persisted activity "${activityId}"`);
19116
+ }
19117
+ return {
19118
+ activityId,
19119
+ acknowledgesLookahead: typeof change === "string" || Boolean(change.fields?.isLookahead),
19120
+ acknowledgesProgress: typeof change !== "string" && Boolean(change.fields?.progress),
19121
+ current,
19122
+ saved
19123
+ };
19124
+ });
19125
+ for (const {
19126
+ activityId,
19127
+ acknowledgesLookahead,
19128
+ acknowledgesProgress,
19129
+ current,
19130
+ saved
19131
+ } of acknowledgements) {
17936
19132
  const persistable = {
17937
19133
  ...saved.persistable,
17938
- isLookahead: current.isLookahead
19134
+ ...acknowledgesLookahead ? { isLookahead: current.isLookahead } : {},
19135
+ ...acknowledgesProgress ? { progress: current.progress } : {}
17939
19136
  };
17940
19137
  this.activitiesAtLastSave.set(activityId, {
17941
19138
  ...saved,
17942
19139
  persistable
17943
19140
  });
17944
- if (current.proplannerId == null || activityChangedSince(current, persistable)) {
19141
+ if (this.initialCalendarRepairIds.has(activityId) || !hasPersistedIdentity(current.proplannerId) || activityChangedSince(current, persistable)) {
17945
19142
  this.dirtyActivityIds.add(activityId);
17946
19143
  } else {
17947
19144
  this.dirtyActivityIds.delete(activityId);
17948
19145
  }
17949
19146
  }
17950
19147
  }
19148
+ hasCheckpointActivityId(activityId) {
19149
+ return this.activitiesAtLastSave.has(activityId);
19150
+ }
19151
+ hasCheckpointLinkId(linkId) {
19152
+ return this.linksAtLastSave.has(linkId);
19153
+ }
17951
19154
  hasUnsavedChanges() {
17952
- return this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
19155
+ return this.trackingEnabled && this.initialized && (this.dirtyActivityIds.size > 0 || this.dirtyLinkIds.size > 0);
19156
+ }
19157
+ newActivities(getActivity) {
19158
+ if (!this.trackingEnabled || !this.initialized) return [];
19159
+ const activities = [];
19160
+ for (const activityId of this.dirtyActivityIds) {
19161
+ const activity = getActivity(activityId);
19162
+ const isNewActivity = activity !== null && !hasPersistedIdentity(activity.proplannerId);
19163
+ if (isNewActivity) activities.push(activity);
19164
+ }
19165
+ return activities;
19166
+ }
19167
+ newLinks(getLink) {
19168
+ if (!this.trackingEnabled || !this.initialized) return [];
19169
+ const links = [];
19170
+ for (const linkId of this.dirtyLinkIds) {
19171
+ const link = getLink(linkId);
19172
+ const isNewLink = link !== null && !hasPersistedIdentity(link.proplannerId);
19173
+ if (isNewLink) links.push(link);
19174
+ }
19175
+ return links;
19176
+ }
19177
+ deletedActivities(getActivity) {
19178
+ if (!this.trackingEnabled || !this.initialized) return [];
19179
+ const deleted = [];
19180
+ for (const activityId of this.dirtyActivityIds) {
19181
+ const saved = this.activitiesAtLastSave.get(activityId);
19182
+ if (saved && getActivity(activityId) === null) {
19183
+ deleted.push({ ...saved.deletedSnapshot });
19184
+ }
19185
+ }
19186
+ return deleted;
17953
19187
  }
17954
- deletedActivities(activities) {
17955
- if (!this.initialized) return [];
17956
- const currentIds = new Set(
17957
- activities.map((activity) => String(activity.id))
17958
- );
19188
+ deletedLinks(getLink) {
19189
+ if (!this.trackingEnabled || !this.initialized) return [];
17959
19190
  const deleted = [];
17960
- for (const [activityId, saved] of this.activitiesAtLastSave) {
17961
- if (currentIds.has(activityId)) continue;
17962
- deleted.push({ ...saved.deletedSnapshot });
19191
+ for (const linkId of this.dirtyLinkIds) {
19192
+ const saved = this.linksAtLastSave.get(linkId);
19193
+ if (saved && getLink(linkId) === null) deleted.push({ ...saved });
17963
19194
  }
17964
19195
  return deleted;
17965
19196
  }
17966
- deletedLinks(links) {
17967
- if (!this.initialized) return [];
17968
- const currentIds = new Set(links.map((link) => String(link.id)));
17969
- return Array.from(
17970
- this.linksAtLastSave.entries(),
17971
- ([linkId, saved]) => currentIds.has(linkId) ? null : { ...saved }
17972
- ).filter((link) => link !== null);
17973
- }
17974
- modifiedActivities(activities) {
17975
- if (!this.initialized) return [];
17976
- return activities.filter((activity) => {
17977
- if (activity.proplannerId == null) return false;
17978
- if (activity.assignedDefaultCalendar) return true;
17979
- const saved = this.activitiesAtLastSave.get(String(activity.id));
17980
- if (!saved) return true;
17981
- return activityChangedSince(activity, saved.persistable);
17982
- });
19197
+ modifiedActivities(getActivity) {
19198
+ if (!this.trackingEnabled || !this.initialized) return [];
19199
+ const modified = [];
19200
+ for (const activityId of this.dirtyActivityIds) {
19201
+ const activity = getActivity(activityId);
19202
+ if (activity === null || !hasPersistedIdentity(activity.proplannerId)) {
19203
+ continue;
19204
+ }
19205
+ const saved = this.activitiesAtLastSave.get(activityId);
19206
+ if (saved === void 0 || this.initialCalendarRepairIds.has(activityId) || activityChangedSince(activity, saved.persistable)) {
19207
+ modified.push(activity);
19208
+ }
19209
+ }
19210
+ return modified;
17983
19211
  }
17984
- modifiedLinks(links) {
17985
- if (!this.initialized) return [];
17986
- return links.filter((link) => {
17987
- if (link.proplannerId == null) return false;
17988
- const saved = this.linksAtLastSave.get(String(link.id));
17989
- if (!saved) return true;
17990
- return linkChangedSince(saved, link);
17991
- });
19212
+ modifiedLinks(getLink) {
19213
+ if (!this.trackingEnabled || !this.initialized) return [];
19214
+ const modified = [];
19215
+ for (const linkId of this.dirtyLinkIds) {
19216
+ const link = getLink(linkId);
19217
+ if (link === null) continue;
19218
+ if (!hasPersistedIdentity(link.proplannerId)) continue;
19219
+ const saved = this.linksAtLastSave.get(linkId);
19220
+ if (saved === void 0 || linkChangedSince(saved, link)) {
19221
+ modified.push(link);
19222
+ }
19223
+ }
19224
+ return modified;
17992
19225
  }
17993
19226
  clear() {
17994
19227
  this.initialized = false;
17995
19228
  this.dirtyActivityIds.clear();
17996
19229
  this.dirtyLinkIds.clear();
19230
+ this.initialCalendarRepairIds.clear();
17997
19231
  this.linksAtLastSave.clear();
17998
19232
  this.activitiesAtLastSave.clear();
17999
19233
  }
@@ -18047,14 +19281,14 @@ function isAutoSchedulable(progress) {
18047
19281
  }
18048
19282
  function parseConstraintLabel(label, reporter) {
18049
19283
  if (!label) return null;
18050
- const normalized = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
18051
- if (normalized === void 0) {
19284
+ const normalized2 = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
19285
+ if (normalized2 === void 0) {
18052
19286
  reporter.warn(
18053
19287
  `[parseFromBackend] unknown constraint label "${label}" \u2014 defaulting to "asap"`
18054
19288
  );
18055
19289
  return DEFAULT_CONSTRAINT_TYPE;
18056
19290
  }
18057
- return normalized;
19291
+ return normalized2;
18058
19292
  }
18059
19293
  function resolveConstraint(raw, reporter) {
18060
19294
  const constraintType = parseConstraintLabel(raw.constraint, reporter);
@@ -18095,6 +19329,24 @@ function parseIdList(raw) {
18095
19329
  function parseParentId(rawParentId) {
18096
19330
  return rawParentId === ROOT_PARENT_ID ? null : String(rawParentId);
18097
19331
  }
19332
+ function normalizePersistedActivityId(rawId) {
19333
+ if (rawId == null) return null;
19334
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19335
+ if (!isValidIdentity) {
19336
+ throw new Error(`invalid activity proplannerId ${String(rawId)}`);
19337
+ }
19338
+ return rawId;
19339
+ }
19340
+ function parseCriticality(raw, reporter) {
19341
+ if (typeof raw.is_critical === "boolean") return raw.is_critical;
19342
+ reporter.error(
19343
+ "[parseFromBackend] invalid activity is_critical \u2014 defaulting to false",
19344
+ new TypeError(
19345
+ `activity "${raw.unique_id}" received ${typeof raw.is_critical}`
19346
+ )
19347
+ );
19348
+ return false;
19349
+ }
18098
19350
  function parseBaselinePoints(raw, reporter) {
18099
19351
  if (!Array.isArray(raw)) return [];
18100
19352
  return raw.map((point) => normalizeBaselinePoint(point, reporter)).filter((point) => point !== null);
@@ -18115,9 +19367,10 @@ function parseActivity(raw, context) {
18115
19367
  const startDate = parseBackendDate(raw.start_date);
18116
19368
  const endDate = isMilestone ? startDate : parseBackendDate(raw.end_date);
18117
19369
  const durationDays = isMilestone ? 0 : Number(raw.duration);
19370
+ const isCritical = parseCriticality(raw, context.reporter);
18118
19371
  return {
18119
19372
  id: String(raw.unique_id),
18120
- proplannerId: typeof raw.id === "number" ? raw.id : null,
19373
+ proplannerId: normalizePersistedActivityId(raw.id),
18121
19374
  parentId: parseParentId(raw.parent_id),
18122
19375
  name: raw.name,
18123
19376
  description: raw.description ?? "",
@@ -18157,7 +19410,12 @@ function parseActivity(raw, context) {
18157
19410
  Number(raw.sumOfDurationRecursively ?? 0),
18158
19411
  context.hoursPerDay
18159
19412
  ),
18160
- isCritical: Boolean(raw.is_critical),
19413
+ calendarDuration: computeCalendarDuration({
19414
+ type: raw.type,
19415
+ startDate,
19416
+ endDate
19417
+ }),
19418
+ isCritical,
18161
19419
  freeSlackHours: null,
18162
19420
  expectedProgress: null,
18163
19421
  expectedProgressBaseline: null,
@@ -18414,6 +19672,14 @@ function resolveLinkType(rawType) {
18414
19672
  if (isLinkTypeCode(rawType)) return rawType;
18415
19673
  return null;
18416
19674
  }
19675
+ function normalizePersistedLinkId(rawId) {
19676
+ if (rawId == null) return void 0;
19677
+ const isValidIdentity = typeof rawId === "number" && (rawId === 0 || Number.isSafeInteger(rawId) && rawId > 0);
19678
+ if (!isValidIdentity) {
19679
+ throw new Error(`invalid link proplannerId ${String(rawId)}`);
19680
+ }
19681
+ return rawId;
19682
+ }
18417
19683
  function normalizeLinks(rawLinks, context) {
18418
19684
  const parsedLinks = [];
18419
19685
  for (const rawLink of rawLinks) {
@@ -18452,16 +19718,18 @@ function parseLink(link, activityIds, hoursPerDay) {
18452
19718
  }
18453
19719
  const ganttIdRaw = link.ganttId;
18454
19720
  const sectorIdRaw = link.sectorId;
18455
- const proplannerIdRaw = link.id;
19721
+ const proplannerId = normalizePersistedLinkId(link.id);
18456
19722
  return {
18457
19723
  id,
18458
19724
  source,
18459
19725
  target,
18460
19726
  type,
18461
19727
  lag: lagDaysToHours(Number(link.lag ?? 0), hoursPerDay),
19728
+ freeSlackHours: null,
19729
+ isCritical: false,
18462
19730
  ...typeof ganttIdRaw === "number" ? { ganttId: ganttIdRaw } : {},
18463
19731
  ...typeof sectorIdRaw === "number" ? { sectorId: sectorIdRaw } : {},
18464
- ...typeof proplannerIdRaw === "number" ? { proplannerId: proplannerIdRaw } : {}
19732
+ ...proplannerId === void 0 ? {} : { proplannerId }
18465
19733
  };
18466
19734
  }
18467
19735
  function dedupeLinksBySignature(links) {
@@ -18521,7 +19789,8 @@ function parseSector(sector) {
18521
19789
  ...customIdIncrement !== void 0 ? { customIdIncrement } : {},
18522
19790
  ...isPrimaveraEndDate ? { updateDurationForPrimaveraEndDate: true } : {},
18523
19791
  activityCreter: parseActivityCreter(sector.activity_creter),
18524
- statusCriteria: DEFAULT_STATUS_CRITERIA
19792
+ statusCriteria: DEFAULT_STATUS_CRITERIA,
19793
+ ganttId: null
18525
19794
  };
18526
19795
  }
18527
19796
 
@@ -18587,6 +19856,9 @@ function createActivityIdGenerator(options = {}) {
18587
19856
  }
18588
19857
  }
18589
19858
  return {
19859
+ fork() {
19860
+ return createActivityIdGenerator({ seed: counter });
19861
+ },
18590
19862
  next() {
18591
19863
  counter += 1;
18592
19864
  return String(counter);
@@ -18618,6 +19890,9 @@ function createLinkIdGenerator(options = {}) {
18618
19890
  }
18619
19891
  }
18620
19892
  return {
19893
+ fork() {
19894
+ return createLinkIdGenerator({ seed: counter });
19895
+ },
18621
19896
  next() {
18622
19897
  counter += 1;
18623
19898
  return String(counter);
@@ -18653,6 +19928,9 @@ function createUniqueCorrelativeIdGenerator(options = {}) {
18653
19928
  }
18654
19929
  }
18655
19930
  return {
19931
+ fork() {
19932
+ return createUniqueCorrelativeIdGenerator({ seed: counter });
19933
+ },
18656
19934
  next() {
18657
19935
  counter += 1;
18658
19936
  return String(counter);
@@ -18780,7 +20058,7 @@ function selectGenerationStrategy(customId, useActivityCustomIdAsReference) {
18780
20058
  function cloneStats(stats) {
18781
20059
  return { ...stats, usedSuffixes: new Set(stats.usedSuffixes) };
18782
20060
  }
18783
- var CustomIdTracker = class {
20061
+ var CustomIdTracker = class _CustomIdTracker {
18784
20062
  prefixMap = /* @__PURE__ */ new Map();
18785
20063
  processedIds = /* @__PURE__ */ new Set();
18786
20064
  _journalPrefixBefore = null;
@@ -18841,6 +20119,19 @@ var CustomIdTracker = class {
18841
20119
  this.defaultPrefix = options.defaultPrefix ?? DEFAULT_PREFIX;
18842
20120
  this.suffixIncrement = options.suffixIncrement ?? DEFAULT_SUFFIX_INCREMENT;
18843
20121
  }
20122
+ fork() {
20123
+ const fork = new _CustomIdTracker({
20124
+ defaultPrefix: this.defaultPrefix,
20125
+ suffixIncrement: this.suffixIncrement
20126
+ });
20127
+ for (const [prefix, stats] of this.prefixMap) {
20128
+ fork.prefixMap.set(prefix, cloneStats(stats));
20129
+ }
20130
+ for (const customId of this.processedIds) {
20131
+ fork.processedIds.add(customId);
20132
+ }
20133
+ return fork;
20134
+ }
18844
20135
  extractPrefix(customId) {
18845
20136
  return extractPrefix(customId);
18846
20137
  }
@@ -19313,6 +20604,7 @@ function recomputeSkippedLeafEnds(state) {
19313
20604
  durationHours: activity.durationHours,
19314
20605
  task: activity
19315
20606
  });
20607
+ activity.calendarDuration = computeCalendarDuration(activity);
19316
20608
  });
19317
20609
  }
19318
20610
  function recomputeDurationsFromDates(state) {
@@ -19384,6 +20676,7 @@ function initializeCore(input) {
19384
20676
  );
19385
20677
  const sector = parsed2.sector;
19386
20678
  sector.statusCriteria = resolveStatusCriteria(input.statusCriteria);
20679
+ sector.ganttId = input.ganttId ?? null;
19387
20680
  const calendars = parsed2.calendars;
19388
20681
  const baseCalendars = parsed2.baseCalendars ?? [];
19389
20682
  const snapshot = buildStateSnapshot(
@@ -19445,6 +20738,133 @@ function initializeCore(input) {
19445
20738
  };
19446
20739
  }
19447
20740
 
20741
+ // src/previsualization/canonical-digest.ts
20742
+ function normalized(value) {
20743
+ if (value === void 0) return ["undefined"];
20744
+ if (value === null) return ["null"];
20745
+ if (typeof value === "string") return ["string", value];
20746
+ if (typeof value === "boolean") return ["boolean", value];
20747
+ if (typeof value === "number") {
20748
+ return ["number", Object.is(value, -0) ? "-0" : String(value)];
20749
+ }
20750
+ if (typeof value === "bigint") return ["bigint", String(value)];
20751
+ if (value instanceof Date) return ["date", String(value.getTime())];
20752
+ if (Array.isArray(value)) return ["array", value.map(normalized)];
20753
+ if (value instanceof Map) {
20754
+ return [
20755
+ "map",
20756
+ Array.from(value, ([key, entry]) => [
20757
+ normalized(key),
20758
+ normalized(entry)
20759
+ ]).sort(
20760
+ (left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))
20761
+ )
20762
+ ];
20763
+ }
20764
+ if (value instanceof Set) {
20765
+ return [
20766
+ "set",
20767
+ Array.from(value, normalized).sort(
20768
+ (left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))
20769
+ )
20770
+ ];
20771
+ }
20772
+ if (typeof value === "object") {
20773
+ return [
20774
+ "object",
20775
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalized(entry)])
20776
+ ];
20777
+ }
20778
+ throw new TypeError(`Unsupported canonical value: ${typeof value}`);
20779
+ }
20780
+ function serializeCanonicalSchedule(value) {
20781
+ return JSON.stringify(normalized(value));
20782
+ }
20783
+
20784
+ // src/previsualization/reconciliation.ts
20785
+ function entityChanges(target, source) {
20786
+ const targetById = new Map(
20787
+ target.map((entity) => [String(entity.id), entity])
20788
+ );
20789
+ const sourceById = new Map(
20790
+ source.map((entity) => [String(entity.id), entity])
20791
+ );
20792
+ const ids = [.../* @__PURE__ */ new Set([...targetById.keys(), ...sourceById.keys()])].sort();
20793
+ const changes = [];
20794
+ for (const id of ids) {
20795
+ const before = sourceById.get(id);
20796
+ const after = targetById.get(id);
20797
+ if (after === void 0) {
20798
+ changes.push({ id, kind: "deleted", after: null });
20799
+ continue;
20800
+ }
20801
+ if (before === void 0) {
20802
+ changes.push({ id, kind: "created", after: cloneDomainValue(after) });
20803
+ continue;
20804
+ }
20805
+ const fields = changedFields(before, after);
20806
+ if (Object.keys(fields).length > 0) {
20807
+ changes.push({
20808
+ id,
20809
+ kind: "updated",
20810
+ fields,
20811
+ after: cloneDomainValue(after)
20812
+ });
20813
+ }
20814
+ }
20815
+ return changes;
20816
+ }
20817
+ function changedFields(before, after) {
20818
+ const fields = {};
20819
+ const keys = [
20820
+ .../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])
20821
+ ].sort();
20822
+ for (const key of keys) {
20823
+ if (serializeCanonicalSchedule(before[key]) !== serializeCanonicalSchedule(after[key])) {
20824
+ fields[key] = {
20825
+ before: cloneDomainValue(before[key]),
20826
+ after: cloneDomainValue(after[key])
20827
+ };
20828
+ }
20829
+ }
20830
+ return fields;
20831
+ }
20832
+ function viewStateChanges(target, source) {
20833
+ const selectedBefore = new Set(source.selectedIds.map(String));
20834
+ const selectedAfter = new Set(target.selectedIds.map(String));
20835
+ const hiddenBefore = new Set(source.hiddenIds.map(String));
20836
+ const hiddenAfter = new Set(target.hiddenIds.map(String));
20837
+ const targetIds = target.activities.map(({ id }) => String(id)).sort();
20838
+ const changes = [];
20839
+ for (const activityId of targetIds) {
20840
+ const checkedBefore = selectedBefore.has(activityId);
20841
+ const checkedAfter = selectedAfter.has(activityId);
20842
+ const visibleBefore = !hiddenBefore.has(activityId);
20843
+ const visibleAfter = !hiddenAfter.has(activityId);
20844
+ if (checkedBefore === checkedAfter && visibleBefore === visibleAfter)
20845
+ continue;
20846
+ changes.push({
20847
+ activityId,
20848
+ ...checkedBefore === checkedAfter ? {} : { checked: { before: checkedBefore, after: checkedAfter } },
20849
+ ...visibleBefore === visibleAfter ? {} : { visible: { before: visibleBefore, after: visibleAfter } }
20850
+ });
20851
+ }
20852
+ return changes;
20853
+ }
20854
+ function createProjectionChangeSet(target, source) {
20855
+ const orderChanged = serializeCanonicalSchedule(target.order) !== serializeCanonicalSchedule(source.order);
20856
+ return {
20857
+ source: { kind: "init" },
20858
+ activities: entityChanges(target.activities, source.activities),
20859
+ links: entityChanges(target.links, source.links),
20860
+ calendars: entityChanges(target.calendars, source.calendars),
20861
+ trackingEvents: [],
20862
+ effects: [],
20863
+ viewState: viewStateChanges(target, source),
20864
+ ...orderChanged ? { order: cloneDomainValue(target.order) } : {}
20865
+ };
20866
+ }
20867
+
19448
20868
  // src/propagations/upward/date-gesture-preview.ts
19449
20869
  var emptyFormula = () => ({
19450
20870
  constantStart: null,
@@ -19590,6 +21010,10 @@ function evaluateEnd(formula, proposedEdgeTimeMs) {
19590
21010
  }
19591
21011
 
19592
21012
  // src/init/schedule-core.ts
21013
+ var FORK_SOURCE = /* @__PURE__ */ Symbol("ScheduleCore.fork.source");
21014
+ function acknowledgesPersistence(action) {
21015
+ return action.kind === "persistence-acknowledge" || action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true;
21016
+ }
19593
21017
  function changesRequireFilterProjectionRebuild(changes) {
19594
21018
  for (const change of changes.activities) {
19595
21019
  if (change.kind === "deleted") return true;
@@ -19613,9 +21037,11 @@ function isCurrentFilterProjection(projection, filter, context) {
19613
21037
  const hasCurrentLocale = projection.context.locale === context.locale;
19614
21038
  return hasCurrentHours && hasCurrentLocale;
19615
21039
  }
19616
- var ScheduleCore = class {
21040
+ var ScheduleCore = class _ScheduleCore {
21041
+ _activityClipboard = { value: null };
19617
21042
  _status = SCHEDULE_CORE_STATUS.READY;
19618
21043
  coreRuntime;
21044
+ _isFork;
19619
21045
  _undo = new UndoRecorder();
19620
21046
  _opQueue = Promise.resolve();
19621
21047
  _scheduleRevision = 0;
@@ -19646,6 +21072,42 @@ var ScheduleCore = class {
19646
21072
  _criticalPathRevision = -1;
19647
21073
  _activeCriticalPath = null;
19648
21074
  constructor(input) {
21075
+ if (FORK_SOURCE in input) {
21076
+ this._isFork = true;
21077
+ const source = input[FORK_SOURCE];
21078
+ const state = source.coreRuntime.state.fork();
21079
+ this.coreRuntime = {
21080
+ state,
21081
+ scheduler: new AutoScheduler(state, source.coreRuntime.reporter),
21082
+ sector: cloneDomainValue(source.coreRuntime.sector),
21083
+ activityIdGenerator: source.coreRuntime.activityIdGenerator.fork(),
21084
+ linkIdGenerator: source.coreRuntime.linkIdGenerator.fork(),
21085
+ uniqueCorrelativeIdGenerator: source.coreRuntime.uniqueCorrelativeIdGenerator.fork(),
21086
+ customIdTracker: source.coreRuntime.customIdTracker.fork(),
21087
+ calendars: new Map(
21088
+ Array.from(source.coreRuntime.calendars, ([id, calendar]) => [
21089
+ id,
21090
+ cloneDomainValue(calendar)
21091
+ ])
21092
+ ),
21093
+ baseCalendars: cloneDomainValue(source.coreRuntime.baseCalendars),
21094
+ clock: source.coreRuntime.clock,
21095
+ reporter: source.coreRuntime.reporter
21096
+ };
21097
+ this._activityClipboard.value = cloneDomainValue(
21098
+ source._activityClipboard.value
21099
+ );
21100
+ this._undo = source._undo.fork();
21101
+ this._saveTracker = source._saveTracker.fork();
21102
+ this._scheduleRevision = source._scheduleRevision;
21103
+ this._pendingScheduleRevision = source._scheduleRevision;
21104
+ this._criticalPathRevision = source._criticalPathRevision;
21105
+ this._initPromise = Promise.resolve();
21106
+ this._criticalPathReady = Promise.resolve(null);
21107
+ this._ready = this._initPromise;
21108
+ return;
21109
+ }
21110
+ this._isFork = false;
19649
21111
  const initialized = initializeCore(input);
19650
21112
  this.coreRuntime = {
19651
21113
  state: initialized.state,
@@ -19682,6 +21144,15 @@ var ScheduleCore = class {
19682
21144
  get ready() {
19683
21145
  return this._ready;
19684
21146
  }
21147
+ fork() {
21148
+ return this._enqueue(async () => {
21149
+ await this._ready;
21150
+ this.assertReady();
21151
+ return new _ScheduleCore({
21152
+ [FORK_SOURCE]: this
21153
+ });
21154
+ });
21155
+ }
19685
21156
  get criticalPathReady() {
19686
21157
  return this._criticalPathReady;
19687
21158
  }
@@ -19709,6 +21180,23 @@ var ScheduleCore = class {
19709
21180
  this.assertReady();
19710
21181
  return readAllActivities(this.coreRuntime.state);
19711
21182
  }
21183
+ /**
21184
+ * Read del plan de indent (ISSUE-080): los padres destino DISTINTOS bajo los
21185
+ * que quedarían las actividades si se indentaran ahora. Es exactamente la
21186
+ * planificación que ejecuta el handler (`planIndentMoves`, autoridad única),
21187
+ * expuesta para que la frontera pueda correr el protocolo de conversión a
21188
+ * madre (chequeo backend + modal) ANTES de despachar. Solo lecturas.
21189
+ */
21190
+ getIndentTargetParentIds(activityIds) {
21191
+ this.assertReady();
21192
+ const plan = planIndentMoves(
21193
+ activityIds.map((id) => String(id)),
21194
+ this.coreRuntime.state
21195
+ );
21196
+ return [
21197
+ ...new Set(plan.allowedMoves.map((move) => String(move.newParentId)))
21198
+ ];
21199
+ }
19712
21200
  getChildrenView(parentId) {
19713
21201
  this.assertReady();
19714
21202
  return readChildren(this.coreRuntime.state, parentId);
@@ -19717,10 +21205,45 @@ var ScheduleCore = class {
19717
21205
  this.assertReady();
19718
21206
  return readLink(this.coreRuntime.state, id);
19719
21207
  }
21208
+ getLinksForActivityView(activityId) {
21209
+ this.assertReady();
21210
+ const state = this.coreRuntime.state;
21211
+ const links = [];
21212
+ const seen = /* @__PURE__ */ new Set();
21213
+ const append = (linkId) => {
21214
+ if (seen.has(linkId)) return;
21215
+ seen.add(linkId);
21216
+ const link = readLink(state, linkId);
21217
+ if (link) links.push(link);
21218
+ };
21219
+ for (const linkId of state.getOutgoingLinkIds(activityId)) append(linkId);
21220
+ for (const linkId of state.getIncomingLinkIds(activityId)) append(linkId);
21221
+ return links;
21222
+ }
19720
21223
  getAllLinksView() {
19721
21224
  this.assertReady();
19722
21225
  return readAllLinks(this.coreRuntime.state);
19723
21226
  }
21227
+ auditEffectiveGraph() {
21228
+ this.assertReady();
21229
+ const state = this.coreRuntime.state;
21230
+ const logicalLinks = state.getAllLinks();
21231
+ const effectiveEdges = projectEffectiveEdges(state, logicalLinks);
21232
+ const components = circularComponents(effectiveEdges);
21233
+ let largestComponentSize = 0;
21234
+ for (const component of components) {
21235
+ if (component.length > largestComponentSize) {
21236
+ largestComponentSize = component.length;
21237
+ }
21238
+ }
21239
+ return {
21240
+ activityCount: state.getAllIds().length,
21241
+ logicalLinkCount: logicalLinks.length,
21242
+ effectiveEdgeCount: effectiveEdges.length,
21243
+ circularComponents: components,
21244
+ largestComponentSize
21245
+ };
21246
+ }
19724
21247
  getProperty(activityId, property) {
19725
21248
  this.assertReady();
19726
21249
  const snapshot = this.coreRuntime.state.getActivity(activityId);
@@ -19749,11 +21272,11 @@ var ScheduleCore = class {
19749
21272
  }
19750
21273
  getActiveFilter() {
19751
21274
  this.assertReady();
19752
- return this.coreRuntime.state.getActiveFilter();
21275
+ return cloneDomainValue(this.coreRuntime.state.getActiveFilter());
19753
21276
  }
19754
21277
  getActiveOrder() {
19755
21278
  this.assertReady();
19756
- return this.coreRuntime.state.getActiveOrder();
21279
+ return cloneDomainValue(this.coreRuntime.state.getActiveOrder());
19757
21280
  }
19758
21281
  getVisualOrderIds() {
19759
21282
  this.assertReady();
@@ -19794,23 +21317,65 @@ var ScheduleCore = class {
19794
21317
  }
19795
21318
  getModifiedLinks() {
19796
21319
  this.assertReady();
19797
- return this._saveTracker.modifiedLinks(this.getAllLinksView());
21320
+ return this._saveTracker.modifiedLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
21321
+ }
21322
+ getNewLinks() {
21323
+ this.assertReady();
21324
+ return this._saveTracker.newLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
19798
21325
  }
19799
21326
  getModifiedActivities() {
19800
21327
  this.assertReady();
19801
- return this._saveTracker.modifiedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
21328
+ return this._saveTracker.modifiedActivities(
21329
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21330
+ ).map(cloneDomainValue);
21331
+ }
21332
+ getNewActivities() {
21333
+ this.assertReady();
21334
+ return this._saveTracker.newActivities(
21335
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21336
+ ).map(cloneDomainValue);
19802
21337
  }
19803
21338
  hasUnsavedChanges() {
19804
21339
  this.assertReady();
19805
21340
  return this._saveTracker.hasUnsavedChanges();
19806
21341
  }
21342
+ /** Read-only revision; moves once per substantive mutation. */
21343
+ getScheduleRevision() {
21344
+ return this._scheduleRevision;
21345
+ }
21346
+ getCanonicalDigest() {
21347
+ this.assertReady();
21348
+ const byId = (left, right) => String(left.id).localeCompare(String(right.id));
21349
+ return serializeCanonicalSchedule({
21350
+ sector: this.getSector(),
21351
+ activities: [...this.getAllActivitiesView()].sort(byId),
21352
+ links: [...this.getAllLinksView()].sort(byId),
21353
+ calendars: [...this.getAllCalendars()].sort(byId),
21354
+ baseCalendars: [...this.getBaseCalendars()].sort(byId)
21355
+ });
21356
+ }
21357
+ createProjectionChangeSetFrom(source) {
21358
+ this.assertReady();
21359
+ source.assertReady();
21360
+ const projectionOf = (core) => ({
21361
+ activities: core.getAllActivitiesView(),
21362
+ links: core.getAllLinksView(),
21363
+ calendars: core.getAllCalendars(),
21364
+ selectedIds: core.getSelectedActivityIds(),
21365
+ hiddenIds: core.getHiddenActivityIds(),
21366
+ order: collectBranchOrder(core.coreRuntime.state)
21367
+ });
21368
+ return createProjectionChangeSet(projectionOf(this), projectionOf(source));
21369
+ }
19807
21370
  getDeletedActivitiesSinceLastSave() {
19808
21371
  this.assertReady();
19809
- return this._saveTracker.deletedActivities(this.coreRuntime.state.getAllActivities()).map(cloneDomainValue);
21372
+ return this._saveTracker.deletedActivities(
21373
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21374
+ ).map(cloneDomainValue);
19810
21375
  }
19811
21376
  getDeletedLinksSinceLastSave() {
19812
21377
  this.assertReady();
19813
- return this._saveTracker.deletedLinks(this.getAllLinksView()).map(cloneDomainValue);
21378
+ return this._saveTracker.deletedLinks((linkId) => this.coreRuntime.state.getLink(linkId)).map(cloneDomainValue);
19814
21379
  }
19815
21380
  _enqueue(work) {
19816
21381
  const workPromise = this._opQueue.then(work, work);
@@ -19905,10 +21470,18 @@ var ScheduleCore = class {
19905
21470
  }
19906
21471
  async _runDispatch(action, options) {
19907
21472
  this.assertReady();
21473
+ if (this._isFork && acknowledgesPersistence(action)) {
21474
+ return {
21475
+ result: {
21476
+ ok: false,
21477
+ reason: REJECTION_REASON.SANDBOX_PERSISTENCE_FORBIDDEN
21478
+ },
21479
+ changedState: false
21480
+ };
21481
+ }
19908
21482
  const pipelineContext = buildPipelineContext(this.coreRuntime.state, {
19909
21483
  hoursPerDay: this.coreRuntime.sector.hoursPerDay,
19910
- dateFormat: this.coreRuntime.sector.dateFormat,
19911
- inputUnit: options.inputUnit ?? "days"
21484
+ dateFormat: this.coreRuntime.sector.dateFormat
19912
21485
  });
19913
21486
  const historyPolicy = getDispatchHistoryPolicy(action);
19914
21487
  const generatorSnapshot = [
@@ -19928,9 +21501,12 @@ var ScheduleCore = class {
19928
21501
  calendars: this.coreRuntime.calendars,
19929
21502
  activityIdGen: this.coreRuntime.activityIdGenerator,
19930
21503
  linkIdGen: this.coreRuntime.linkIdGenerator,
21504
+ isActivityIdReserved: (activityId) => this._saveTracker.hasCheckpointActivityId(activityId),
21505
+ isLinkIdReserved: (linkId) => this._saveTracker.hasCheckpointLinkId(linkId),
19931
21506
  uidGen: this.coreRuntime.uniqueCorrelativeIdGenerator,
19932
21507
  customIdTracker: this.coreRuntime.customIdTracker,
19933
21508
  reporter: this.coreRuntime.reporter,
21509
+ activityClipboard: this._activityClipboard,
19934
21510
  defaultBaseCalendarId: this.coreRuntime.baseCalendars.find(
19935
21511
  (calendar) => calendar.baseDefault
19936
21512
  )?.id ?? null,
@@ -19963,16 +21539,19 @@ var ScheduleCore = class {
19963
21539
  }
19964
21540
  if (result.ok) {
19965
21541
  if (historyPolicy === "clear-on-success") {
19966
- this._saveTracker.snapshotActivities(
19967
- this.coreRuntime.state.getAllActivities()
21542
+ this._saveTracker.acknowledge(
21543
+ this.coreRuntime.state.getAllActivities(),
21544
+ this.coreRuntime.state.getAllLinks()
19968
21545
  );
19969
- this._saveTracker.snapshotLinks(this.getAllLinksView());
19970
21546
  this._undo.clear();
19971
21547
  } else if (action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true) {
19972
21548
  this._saveTracker.acknowledgePersistedLookahead(
19973
- action.activityIds,
21549
+ result.changes.activities,
19974
21550
  (activityId) => this.coreRuntime.state.getActivity(activityId)
19975
21551
  );
21552
+ if (action.persistedProgress !== void 0) {
21553
+ this._undo.clear();
21554
+ }
19976
21555
  } else {
19977
21556
  this._saveTracker.recordChanges(
19978
21557
  result.changes.activities,
@@ -20236,13 +21815,20 @@ var ScheduleCore = class {
20236
21815
  this.coreRuntime.customIdTracker.beginCustomIdTransaction();
20237
21816
  let changes;
20238
21817
  try {
20239
- for (const [activityId, fields] of calculatedFields) {
21818
+ for (const [activityId, fields] of calculatedFields.activities) {
20240
21819
  setCriticalPathFieldsIfChanged(
20241
21820
  this.coreRuntime.state,
20242
21821
  activityId,
20243
21822
  fields
20244
21823
  );
20245
21824
  }
21825
+ for (const [linkId, fields] of calculatedFields.links) {
21826
+ setCriticalPathLinkFieldsIfChanged(
21827
+ this.coreRuntime.state,
21828
+ linkId,
21829
+ fields
21830
+ );
21831
+ }
20246
21832
  changes = await assembleChangeSet(this.coreRuntime.state, {
20247
21833
  source: { kind: "init" },
20248
21834
  beforeSnap: /* @__PURE__ */ new Map(),
@@ -20498,6 +22084,9 @@ var DEFAULT_HOURS_PER_DAY = 8;
20498
22084
 
20499
22085
  // src/dispatch/action-kinds.ts
20500
22086
  var DISPATCH_ACTION_KIND = {
22087
+ ACTIVITY_CLIPBOARD_CLEAR: "activity-clipboard-clear",
22088
+ ACTIVITY_COPY: "activity-copy",
22089
+ ACTIVITY_PASTE_COPIED: "activity-paste-copied",
20501
22090
  PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
20502
22091
  BASELINE_APPLY: "baseline-apply",
20503
22092
  PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
@@ -20515,7 +22104,6 @@ var DISPATCH_ACTION_KIND = {
20515
22104
  ACTIVITY_INDENT: "activity-indent",
20516
22105
  ACTIVITY_OUTDENT: "activity-outdent",
20517
22106
  ACTIVITY_SET_PROGRESS: "activity-set-progress",
20518
- ACTIVITY_PASTE: "activity-paste",
20519
22107
  SELECTION_TOGGLE: "selection-toggle",
20520
22108
  SELECTION_REPLACE: "selection-replace",
20521
22109
  VISIBILITY_SET: "visibility-set",
@@ -20525,6 +22113,9 @@ var DISPATCH_ACTION_KIND = {
20525
22113
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
20526
22114
  };
20527
22115
  var KIND_CATALOG_COVERS_UNION = {
22116
+ [DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_CLEAR]: true,
22117
+ [DISPATCH_ACTION_KIND.ACTIVITY_COPY]: true,
22118
+ [DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED]: true,
20528
22119
  [DISPATCH_ACTION_KIND.PERSISTENCE_ACKNOWLEDGE]: true,
20529
22120
  [DISPATCH_ACTION_KIND.BASELINE_APPLY]: true,
20530
22121
  [DISPATCH_ACTION_KIND.PONDERATOR_CRITERION_SET]: true,
@@ -20542,7 +22133,6 @@ var KIND_CATALOG_COVERS_UNION = {
20542
22133
  [DISPATCH_ACTION_KIND.ACTIVITY_INDENT]: true,
20543
22134
  [DISPATCH_ACTION_KIND.ACTIVITY_OUTDENT]: true,
20544
22135
  [DISPATCH_ACTION_KIND.ACTIVITY_SET_PROGRESS]: true,
20545
- [DISPATCH_ACTION_KIND.ACTIVITY_PASTE]: true,
20546
22136
  [DISPATCH_ACTION_KIND.SELECTION_TOGGLE]: true,
20547
22137
  [DISPATCH_ACTION_KIND.SELECTION_REPLACE]: true,
20548
22138
  [DISPATCH_ACTION_KIND.VISIBILITY_SET]: true,